// 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.paymentcarddetailsform import android.annotation.SuppressLint import android.graphics.Typeface import android.os.Build import android.util.AttributeSet import android.util.TypedValue 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.olo.olopay.controls.PaymentCardDetailsForm import com.olo.olopay.controls.callbacks.FormValidCallback import com.olo.olopay.data.CardField import com.olo.olopay.exceptions.OloPayException import com.olopaysdkreactnative.R import com.olopaysdkreactnative.data.DataKeys import com.olopaysdkreactnative.extensions.rejectException import com.olopaysdkreactnative.extensions.toMap import com.olopaysdkreactnative.data.ErrorCodes import com.olopaysdkreactnative.data.FontWeight import com.olopaysdkreactnative.data.GlobalConstants import com.olopaysdkreactnative.events.FormValidEvent import com.olopaysdkreactnative.extensions.getBoolean import com.olopaysdkreactnative.extensions.getNullableInt import com.olopaysdkreactnative.extensions.getNullableString import com.olopaysdkreactnative.extensions.getString @SuppressLint("ViewConstructor") class PaymentCardDetailsForm constructor( context: ThemedReactContext, attrs: AttributeSet? = null, defStyleAttr: Int = 0): ConstraintLayout(context, attrs, defStyleAttr), FormValidCallback { private val _cardDetails: PaymentCardDetailsForm private var _cachedFormValidState = false private val reactContext: ThemedReactContext get() = context as ThemedReactContext private val surfaceId get() = UIManagerHelper.getSurfaceId(reactContext) init { inflate(context, R.layout.rnsdk_form_view, this) _cardDetails = findViewById(R.id.rn_form_view) _cardDetails.formValidCallback = this viewTreeObserver.addOnGlobalLayoutListener { requestLayout() } } override fun setEnabled(enabled: Boolean) { _cardDetails.isEnabled = enabled } fun requestFocusFromJS(field:CardField, showKeyboard: Boolean) { _cardDetails.requestFocus(field, showKeyboard) } fun clearFocusFromJS() { _cardDetails.dismissKeyboard() } fun clearCardDetails() { _cardDetails.clearFields() _cardDetails.requestFocus(CardField.CardNumber, false) //TODO Ticket OLO-103624 } fun setPlaceholders(placeholders: ReadableMap) { val numberPlaceholder = placeholders.getString(DataKeys.NumberKey, "") val expirationPlaceholder = placeholders.getString(DataKeys.ExpirationKey, "") val cvvPlaceholder = placeholders.getString(DataKeys.CvvKey, "") val postalCodePlaceholder = placeholders.getString(DataKeys.PostalCodeKey, "") if (numberPlaceholder.isNotEmpty()) _cardDetails.setHintText(CardField.CardNumber, numberPlaceholder) if (cvvPlaceholder.isNotEmpty()) _cardDetails.setHintText(CardField.Cvv ,cvvPlaceholder) if (expirationPlaceholder.isNotEmpty()) _cardDetails.setHintText(CardField.Expiration, expirationPlaceholder) if (postalCodePlaceholder.isNotEmpty()) _cardDetails.setHintText(CardField.PostalCode, postalCodePlaceholder) } fun setCardStyles(cardStyles: ReadableMap) { val backgroundColor = cardStyles.getNullableString(DataKeys.BackgroundColorKey) val borderColor = cardStyles.getNullableString(DataKeys.BorderColorKey) val cursorColor = cardStyles.getString(DataKeys.CursorColorKey, "") val dividerColor = cardStyles.getString(DataKeys.FieldDividerColorKey, "") val fontFamily = cardStyles.getNullableString(DataKeys.FontFamilyKey) val fontWeight = cardStyles.getNullableString(DataKeys.FontWeightKey) val italic = cardStyles.getBoolean(DataKeys.ItalicKey, false) val hintTextColor = cardStyles.getString(DataKeys.PlaceholderColorKey, "") val textSize = cardStyles.getNullableInt(DataKeys.FontSizeKey) val textColor = cardStyles.getString(DataKeys.TextColorKey, "") val focusedHintTextColor = cardStyles.getString(DataKeys.FocusedPlaceholderColorKey, "") val displayMetrics = _cardDetails.context.resources.displayMetrics val borderWidth = cardStyles.getNullableInt(DataKeys.BorderWidthKey)?.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() } val dividerWidth = cardStyles.getNullableInt(DataKeys.FieldDividerWidthKey)?.let { TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, it.toFloat(), displayMetrics).toInt() } val elevation = cardStyles.getNullableInt(DataKeys.CardElevationKey)?.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() } if(textSize != null) _cardDetails.setTextSize(textSize.toFloat()) if(cursorColor.isNotEmpty() && Build.VERSION.SDK_INT >= GlobalConstants.ApiQuinceTart) _cardDetails.setCursorColor(cursorColor) if(Build.VERSION.SDK_INT >= GlobalConstants.ApiOreo) { _cardDetails.setFieldDividerColor(dividerColor) } if(Build.VERSION.SDK_INT >= GlobalConstants.ApiOreo){ _cardDetails.setTextColor(textColor) _cardDetails.setHintTextColor(hintTextColor) } if(Build.VERSION.SDK_INT >= GlobalConstants.ApiOreo){ _cardDetails.setFocusedHintTextColor(focusedHintTextColor) } if (dividerWidth != null) { _cardDetails.setFieldDividerWidth(dividerWidth) } 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) } _cardDetails.setFont(typeface) } _cardDetails.setCardPadding( startPx = paddingLeft, topPx = null, endPx = paddingRight, bottomPx = null) elevation?.toFloat()?.let { _cardDetails.setCardElevation( elevationPx = it ) } if(Build.VERSION.SDK_INT >= GlobalConstants.ApiOreo) { _cardDetails.setCardBackgroundStyle( backgroundColorHex = backgroundColor, borderColorHex = borderColor, borderWidthPx = borderWidth?.toFloat(), borderRadiusPx = cornerRadius?.toFloat()) } } suspend fun createPaymentMethod(promise: Promise) { val params = _cardDetails.paymentMethodParams if (params == null) { promise.reject(ErrorCodes.InvalidCardDetails, "Card details are invalid") return } try { val paymentMethod = OloPayAPI().createPaymentMethod(context, params) promise.resolve(paymentMethod.toMap()) } catch (e: OloPayException) { promise.rejectException(e) } } override fun onInputChanged(isValid: Boolean, invalidFields: Set) { if(isValid != _cachedFormValidState) { _cachedFormValidState = isValid val event = FormValidEvent(surfaceId, id, isValid) event.emit(reactContext) } } override fun requestLayout() { super.requestLayout() post(mLayoutRunnable) } private val mLayoutRunnable = Runnable { measure( MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)) layout(left, top, right, bottom) } }