// Copyright © 2022 Olo Inc. All rights reserved. // This software is made available under the Olo Pay SDK License (See LICENSE.md file) package com.olopaysdkreactnative.paymentcardcvvview import android.annotation.SuppressLint import android.graphics.Typeface import android.os.Build import android.util.AttributeSet import android.util.TypedValue import android.view.Gravity import androidx.constraintlayout.widget.ConstraintLayout import com.facebook.react.bridge.Arguments.createMap import com.facebook.react.bridge.ReadableMap import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.UIManagerHelper import com.olo.olopay.api.OloPayAPI import com.olopaysdkreactnative.R import com.olo.olopay.controls.PaymentCardCvvView import com.olo.olopay.controls.callbacks.CvvInputListener import com.olo.olopay.data.CardField import com.olo.olopay.data.ICardFieldState import com.olo.olopay.exceptions.OloPayException import com.olopaysdkreactnative.data.* import com.olopaysdkreactnative.extensions.* import com.olopaysdkreactnative.events.CvvTokenResultEvent import com.olopaysdkreactnative.paymentcardcvvview.events.CvvDetailsChangedEvent import com.olopaysdkreactnative.paymentcardcvvview.events.CvvFocusClearedEvent import com.olopaysdkreactnative.paymentcardcvvview.events.CvvFocusReceivedEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import kotlin.coroutines.CoroutineContext @SuppressLint("ViewConstructor") class PaymentCardCvvView @JvmOverloads constructor( context: ThemedReactContext, attrs: AttributeSet? = null ): ConstraintLayout(context, attrs), CvvInputListener, CoroutineScope { private var job = SupervisorJob() override val coroutineContext: CoroutineContext get() = Dispatchers.Main + job override fun onAttachedToWindow() { super.onAttachedToWindow() // Recreate job if it was cancelled (e.g., view was detached and reattached) if (job.isCancelled) { job = SupervisorJob() } } private val _cvvDetails: PaymentCardCvvView private val reactContext: ThemedReactContext get() = context as ThemedReactContext private val surfaceId get() = UIManagerHelper.getSurfaceId(reactContext) private var _customErrorMessages: CustomErrorMessages? = null init { inflate(context, R.layout.rnsdk_cvv_view, this) _cvvDetails = findViewById(R.id.rn_cvv_view) _cvvDetails.cvvInputListener = this viewTreeObserver.addOnGlobalLayoutListener { requestLayout() } } override fun setEnabled(enabled: Boolean) { _cvvDetails.isEnabled = enabled } fun createCvvUpdateToken() = launch { val params = _cvvDetails.cvvTokenParams val eventData = createMap() if (params == null) { val errorMessage = getErrorMessage(false) val errorData = createMap() errorData.putString(DataKeys.CodeKey, ErrorCodes.InvalidCvv) errorData.putString(DataKeys.MessageKey, errorMessage) eventData.putMap(DataKeys.ErrorKey, errorData) CvvTokenResultEvent(surfaceId, id, eventData).emit(reactContext) return@launch } try { eventData.putMap( DataKeys.CvvTokenKey, OloPayAPI().createCvvUpdateToken(context, params).toMap() ) } catch (e: OloPayException) { eventData.putMap(DataKeys.ErrorKey, e.toResultErrorMap()) } CvvTokenResultEvent(surfaceId, id, eventData).emit(reactContext) } fun requestFocusFromJS(showKeyboard: Boolean) { _cvvDetails.requestFocus(showKeyboard) } fun clearFocusFromJS() { _cvvDetails.dismissKeyboard() } fun clearCvvDetails() { _cvvDetails.clear() } override fun onInputChanged(state: ICardFieldState) { val editedFieldError = getErrorMessage(true) val uneditedFieldError = getErrorMessage(false) CvvDetailsChangedEvent(surfaceId, id, state, editedFieldError, uneditedFieldError).emit(reactContext) } override fun onFocusChange(state: ICardFieldState) { val editedFieldError = getErrorMessage(true) val uneditedFieldError = getErrorMessage(false) val cvvEvent = if (state.isFocused) { CvvFocusReceivedEvent(surfaceId, id, state, editedFieldError, uneditedFieldError) } else { CvvFocusClearedEvent(surfaceId, id, state, editedFieldError, uneditedFieldError) } cvvEvent.emit(reactContext) } fun setCustomErrorMessages(customErrorMessages: ReadableMap) { val invalidCvvError = customErrorMessages.getNullableString(DataKeys.InvalidErrorKey) val emptyCvvError = customErrorMessages.getNullableString(DataKeys.EmptyErrorKey) _customErrorMessages = CustomErrorMessages(invalidCvvError, emptyCvvError) onInputChanged(_cvvDetails.fieldState) } fun setCvvStyles(cardStyles: ReadableMap) { val backgroundColor = cardStyles.getNullableString(DataKeys.BackgroundColorKey) val borderColor = cardStyles.getNullableString(DataKeys.BorderColorKey) val cursorColor = cardStyles.getString(DataKeys.CursorColorKey, "") val errorTextColor = cardStyles.getString(DataKeys.ErrorTextColorKey, "") val fontFamily = cardStyles.getNullableString(DataKeys.FontFamilyKey) val fontWeight = cardStyles.getNullableString(DataKeys.FontWeightKey) val hintTextColor = cardStyles.getString(DataKeys.PlaceholderColorKey, "") val italic = cardStyles.getBoolean(DataKeys.ItalicKey, false) val textSize = cardStyles.getNullableInt(DataKeys.FontSizeKey) val textColor = cardStyles.getString(DataKeys.TextColorKey, "") val textAlign = cardStyles.getNullableString(DataKeys.TextAlignKey) val displayMetrics = _cvvDetails.context.resources.displayMetrics val borderWidth = cardStyles.getNullableInt(DataKeys.BorderWidthKey)?.let { TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, it.toFloat(), displayMetrics).toInt() } val cornerRadius = cardStyles.getNullableInt(DataKeys.CornerRadiusKey)?.let { TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, it.toFloat(), displayMetrics).toInt() } val paddingLeft = cardStyles.getNullableInt(DataKeys.TextPaddingLeftKey)?.let { TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, it.toFloat(), displayMetrics).toInt() } val paddingRight = cardStyles.getNullableInt(DataKeys.TextPaddingRightKey)?.let { TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, it.toFloat(), displayMetrics).toInt() } if(Build.VERSION.SDK_INT >= GlobalConstants.ApiOreo){ _cvvDetails.setTextColor(textColor) _cvvDetails.setErrorTextColor(errorTextColor) _cvvDetails.setHintTextColor(hintTextColor) } if(textSize != null) _cvvDetails.setTextSize(textSize.toFloat()) if(cursorColor.isNotEmpty() && Build.VERSION.SDK_INT >= GlobalConstants.ApiQuinceTart) _cvvDetails.setCursorColor(cursorColor) if(!fontFamily.isNullOrEmpty() || !fontWeight.isNullOrEmpty() || !cardStyles.hasKey(DataKeys.ItalicKey)) { val fontWeightValue = FontWeight.convertFrom(fontWeight).value val fontStyle = if(italic){ if(fontWeightValue >= FontWeight.Bold.value) Typeface.BOLD_ITALIC else Typeface.ITALIC } else if (fontWeightValue >= FontWeight.Bold.value) { Typeface.BOLD } else { Typeface.NORMAL } var typeface = if(!fontFamily.isNullOrEmpty()) { Typeface.create(fontFamily, fontStyle) } else { Typeface.create(Typeface.DEFAULT, fontStyle) } if(Build.VERSION.SDK_INT >= GlobalConstants.ApiPie) { typeface = Typeface.create(typeface, fontWeightValue, italic) } _cvvDetails.setFont(typeface) } _cvvDetails.setCvvPadding( startPx = paddingLeft, topPx = null, endPx = paddingRight, bottomPx = null) if(Build.VERSION.SDK_INT >= GlobalConstants.ApiOreo) { _cvvDetails.setCvvBackgroundStyle( backgroundColorHex = backgroundColor, borderColorHex = borderColor, borderWidthPx = borderWidth?.toFloat(), borderRadiusPx = cornerRadius?.toFloat()) } if(!textAlign.isNullOrEmpty()) { val position = when(textAlign) { DataKeys.GravityCenterKey -> Gravity.CENTER DataKeys.GravityRightKey -> Gravity.END or Gravity.CENTER_VERTICAL else -> Gravity.START or Gravity.CENTER_VERTICAL } _cvvDetails.setGravity(position) } } fun setPlaceholder(placeholder: String) { _cvvDetails.setHintText(placeholder) } private fun getErrorMessage(ignoreUneditedFields: Boolean): String { if (_cvvDetails.isValid || !_cvvDetails.hasErrorMessage(ignoreUneditedFields)) { return "" } val defaultErrorMessage = _cvvDetails.getErrorMessage(ignoreUneditedFields) val fieldData = mapOf(CardField.Cvv to _cvvDetails.fieldState) return _customErrorMessages?.getCustomErrorMessage(ignoreUneditedFields, fieldData) ?: defaultErrorMessage } override fun onDetachedFromWindow() { super.onDetachedFromWindow() job.cancel() // Cancel all coroutines when view is detached } }