// 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.Promise 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.paymentcardcvvview.events.CvvDetailsChangedEvent import com.olopaysdkreactnative.paymentcardcvvview.events.CvvFocusClearedEvent import com.olopaysdkreactnative.paymentcardcvvview.events.CvvFocusReceivedEvent @SuppressLint("ViewConstructor") class PaymentCardCvvView @JvmOverloads constructor( context: ThemedReactContext, attrs: AttributeSet? = null ): ConstraintLayout(context, attrs), CvvInputListener { 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 } suspend fun createCvvUpdateToken(promise: Promise) { val params = _cvvDetails.cvvTokenParams if (params == null) { val errorMessage = getErrorMessage(false) promise.reject(ErrorCodes.InvalidCvv, errorMessage) return } try { val cvvUpdateToken = OloPayAPI().createCvvUpdateToken(context, params) promise.resolve(cvvUpdateToken.toMap()) } catch (e: OloPayException) { promise.rejectException(e) } } 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("invalidError") val emptyCvvError = customErrorMessages.getNullableString("emptyError") _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 } }