Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Multiline card element #415

Merged
merged 14 commits into from
Aug 3, 2021
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.reactnativestripesdk

import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event
import com.facebook.react.uimanager.events.RCTEventEmitter

internal class CardFormCompleteEvent constructor(viewTag: Int, private val cardDetails: MutableMap<String, Any>?, private val complete: Boolean, private val dangerouslyGetFullCardDetails: Boolean) : Event<CardChangedEvent>(viewTag) {
override fun getEventName(): String {
return EVENT_NAME
}

override fun dispatch(rctEventEmitter: RCTEventEmitter) {
rctEventEmitter.receiveEvent(viewTag, eventName, serializeEventData())
}

private fun serializeEventData(): WritableMap {
val eventData = Arguments.createMap()

if (cardDetails == null) {
return eventData
}
eventData.putString("brand", cardDetails["brand"]?.toString())
eventData.putString("last4", cardDetails["last4"]?.toString())
eventData.putString("country", cardDetails["country"]?.toString())
eventData.putInt("expiryMonth", cardDetails["expiryMonth"] as Int)
eventData.putInt("expiryYear", cardDetails["expiryYear"] as Int)
eventData.putBoolean("complete", complete)
eventData.putString("postalCode", cardDetails["postalCode"]?.toString())

if (dangerouslyGetFullCardDetails) {
eventData.putString("number", cardDetails["number"]?.toString()?.replace(" ", ""))
}

return eventData
}

companion object {
const val EVENT_NAME = "onFormComplete"
}

}
190 changes: 190 additions & 0 deletions android/src/main/java/com/reactnativestripesdk/CardFormView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package com.reactnativestripesdk

import android.content.res.ColorStateList
import android.graphics.Color
import android.view.View
import android.view.View.OnFocusChangeListener
import android.widget.FrameLayout
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.UIManagerModule
import com.facebook.react.uimanager.events.EventDispatcher
import com.google.android.material.shape.MaterialShapeDrawable
import com.stripe.android.databinding.CardMultilineWidgetBinding
import com.stripe.android.databinding.StripeCardFormViewBinding
import com.stripe.android.model.Address
import com.stripe.android.model.PaymentMethodCreateParams
import com.stripe.android.view.CardFormView
import com.stripe.android.view.CardInputListener

class CardFormView(private val context: ThemedReactContext) : FrameLayout(context) {
private var cardForm: CardFormView = CardFormView(context, null, R.style.StripeCardFormView_Borderless)
private var mEventDispatcher: EventDispatcher? = context.getNativeModule(UIManagerModule::class.java)?.eventDispatcher
private var dangerouslyGetFullCardDetails: Boolean = false
private var currentFocusedField: String? = null
var cardParams: PaymentMethodCreateParams.Card? = null
var cardAddress: Address? = null
private val cardFormViewBinding = StripeCardFormViewBinding.bind(cardForm)
private val multilineWidgetBinding = CardMultilineWidgetBinding.bind(cardFormViewBinding.cardMultilineWidget)

init {
cardFormViewBinding.cardMultilineWidgetContainer.isFocusable = true
cardFormViewBinding.cardMultilineWidgetContainer.isFocusableInTouchMode = true
cardFormViewBinding.cardMultilineWidgetContainer.requestFocus()

addView(cardForm)
setListeners()

viewTreeObserver.addOnGlobalLayoutListener { requestLayout() }
}

fun setPostalCodeEnabled(value: Boolean) {
val cardFormView = StripeCardFormViewBinding.bind(cardForm)
val visibility = if (value) View.VISIBLE else View.GONE

cardFormView.cardMultilineWidget.postalCodeRequired = false
cardFormView.postalCodeContainer.visibility = visibility
}

fun setPlaceHolders(value: ReadableMap) {
val cardFormView = StripeCardFormViewBinding.bind(cardForm)

val numberPlaceholder = getValOr(value, "number", null)
val expirationPlaceholder = getValOr(value, "expiration", null)
val cvcPlaceholder = getValOr(value, "cvc", null)
val postalCodePlaceholder = getValOr(value, "postalCode", null)

numberPlaceholder?.let {
// multilineWidgetBinding.tlCardNumber.hint = it
}
expirationPlaceholder?.let {
multilineWidgetBinding.tlExpiry.hint = it
}
cvcPlaceholder?.let {
multilineWidgetBinding.tlCvc.hint = it
}
postalCodePlaceholder?.let {
cardFormView.postalCodeContainer.hint = it
}
}

fun setAutofocus(value: Boolean) {
if (value) {
val cardNumberEditText = multilineWidgetBinding.etCardNumber
cardNumberEditText.requestFocus()
cardNumberEditText.showSoftKeyboard()
}
}

fun requestFocusFromJS() {
val cardNumberEditText = multilineWidgetBinding.etCardNumber
cardNumberEditText.requestFocus()
cardNumberEditText.showSoftKeyboard()
}

fun requestBlurFromJS() {
val cardNumberEditText = multilineWidgetBinding.etCardNumber
cardNumberEditText.hideSoftKeyboard()
cardNumberEditText.clearFocus()
}

fun requestClearFromJS() {
multilineWidgetBinding.etCardNumber.setText("")
multilineWidgetBinding.etCvc.setText("")
multilineWidgetBinding.etExpiry.setText("")
cardFormViewBinding.postalCode.setText("")
}

private fun onChangeFocus() {
mEventDispatcher?.dispatchEvent(
CardFocusEvent(id, currentFocusedField))
}

fun setCardStyle(value: ReadableMap) {
val binding = StripeCardFormViewBinding.bind(cardForm)
val backgroundColor = getValOr(value, "backgroundColor", null)

binding.cardMultilineWidgetContainer.background = MaterialShapeDrawable().also { shape ->
shape.fillColor = ColorStateList.valueOf(Color.parseColor("#FFFFFF"))
backgroundColor?.let {
shape.fillColor = ColorStateList.valueOf(Color.parseColor(it))
}
}
}

fun setDangerouslyGetFullCardDetails(isEnabled: Boolean) {
dangerouslyGetFullCardDetails = isEnabled
}

private fun setListeners() {
cardForm.setCardValidCallback { isValid, _ ->
if (isValid) {
cardForm.cardParams?.let {
val cardParamsMap = it.toParamMap()["card"] as HashMap<*, *>
val cardDetails: MutableMap<String, Any> = mutableMapOf(
"expiryMonth" to cardParamsMap["exp_month"] as Int,
"expiryYear" to cardParamsMap["exp_year"] as Int,
"last4" to it.last4,
"brand" to mapCardBrand(it.brand),
"postalCode" to (it.address?.postalCode ?: ""),
"country" to (it.address?.country ?: "")
)

if (dangerouslyGetFullCardDetails) {
cardDetails["number"] = cardParamsMap["number"] as String
}

mEventDispatcher?.dispatchEvent(
CardFormCompleteEvent(id, cardDetails, isValid, dangerouslyGetFullCardDetails))

cardAddress = Address.Builder()
.setPostalCode(it.address?.postalCode)
.setCountry(it.address?.country)
.build()

val binding = StripeCardFormViewBinding.bind(cardForm)
binding.cardMultilineWidget.paymentMethodCard?.let { params -> cardParams = params }
}
} else {
cardParams = null
cardAddress = null
mEventDispatcher?.dispatchEvent(
CardFormCompleteEvent(id, null, isValid, dangerouslyGetFullCardDetails))
}
}

val cardNumberEditText = multilineWidgetBinding.etCardNumber
val cvcEditText = multilineWidgetBinding.etCvc
val expiryEditText = multilineWidgetBinding.etExpiry
val postalCodeEditText = cardFormViewBinding.postalCode

cardNumberEditText.onFocusChangeListener = OnFocusChangeListener { _, hasFocus ->
currentFocusedField = if (hasFocus) CardInputListener.FocusField.CardNumber.toString() else null
onChangeFocus()
}
cvcEditText.onFocusChangeListener = OnFocusChangeListener { _, hasFocus ->
currentFocusedField = if (hasFocus) CardInputListener.FocusField.Cvc.toString() else null
onChangeFocus()
}
expiryEditText.onFocusChangeListener = OnFocusChangeListener { _, hasFocus ->
currentFocusedField = if (hasFocus) CardInputListener.FocusField.ExpiryDate.toString() else null
onChangeFocus()
}
postalCodeEditText.onFocusChangeListener = OnFocusChangeListener { _, hasFocus ->
currentFocusedField = if (hasFocus) CardInputListener.FocusField.PostalCode.toString() else null
onChangeFocus()
}
}

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)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.reactnativestripesdk

import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.common.MapBuilder
import com.facebook.react.uimanager.ReactStylesDiffMap
import com.facebook.react.uimanager.SimpleViewManager
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.annotations.ReactProp

class CardFormViewManager : SimpleViewManager<CardFormView>() {
override fun getName() = "CardForm"

private var reactContextRef: ThemedReactContext? = null

override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> {
return MapBuilder.of(
CardFocusEvent.EVENT_NAME, MapBuilder.of("registrationName", "onFocusChange"),
CardFormCompleteEvent.EVENT_NAME, MapBuilder.of("registrationName", "onFormComplete"))
}

override fun receiveCommand(root: CardFormView, commandId: String?, args: ReadableArray?) {
when (commandId) {
"focus" -> root.requestFocusFromJS()
"blur" -> root.requestBlurFromJS()
"clear" -> root.requestClearFromJS()
}
}

@ReactProp(name = "dangerouslyGetFullCardDetails")
fun setDangerouslyGetFullCardDetails(view: CardFormView, dangerouslyGetFullCardDetails: Boolean = false) {
view.setDangerouslyGetFullCardDetails(dangerouslyGetFullCardDetails);
}

@ReactProp(name = "postalCodeEnabled")
fun setPostalCodeEnabled(view: CardFormView, postalCodeEnabled: Boolean = false) {
view.setPostalCodeEnabled(postalCodeEnabled);
}

// @ReactProp(name = "placeholder")
// fun setPlaceHolders(view: CardFormView, placeholder: ReadableMap) {
// view.setPlaceHolders(placeholder);
// }

@ReactProp(name = "autofocus")
fun setAutofocus(view: CardFormView, autofocus: Boolean = false) {
view.setAutofocus(autofocus);
}

@ReactProp(name = "cardStyle")
fun setCardStyle(view: CardFormView, cardStyle: ReadableMap) {
view.setCardStyle(cardStyle);
}

override fun createViewInstance(reactContext: ThemedReactContext): CardFormView {
val stripeSdkModule: StripeSdkModule? = reactContext.getNativeModule(StripeSdkModule::class.java)
val view = CardFormView(reactContext)

reactContextRef = reactContext

stripeSdkModule?.cardFormView = view
return view
}

override fun onDropViewInstance(view: CardFormView) {
super.onDropViewInstance(view)

val stripeSdkModule: StripeSdkModule? = reactContextRef?.getNativeModule(StripeSdkModule::class.java)
stripeSdkModule?.cardFormView = null
reactContextRef = null
}
}
21 changes: 21 additions & 0 deletions android/src/main/java/com/reactnativestripesdk/Extensions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.reactnativestripesdk

import android.content.Context
import android.view.View
import android.view.inputmethod.InputMethodManager

fun View.showSoftKeyboard() {
post {
if (this.requestFocus()) {
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?
imm?.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT)
}
}
}

fun View.hideSoftKeyboard() {
if (this.requestFocus()) {
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?
imm?.hideSoftInputFromWindow(windowToken, 0)
}
}
30 changes: 24 additions & 6 deletions android/src/main/java/com/reactnativestripesdk/Mappers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -394,21 +394,31 @@ fun getValOr(map: ReadableMap, key: String, default: String? = ""): String? {
return if (map.hasKey(key)) map.getString(key) else default
}

internal fun mapToAddress(addressMap: ReadableMap?): Address? {
internal fun mapToAddress(addressMap: ReadableMap?, cardAddress: Address?): Address? {
if (addressMap == null) {
return null
}
return Address.Builder()
val address = Address.Builder()
.setPostalCode(getValOr(addressMap, "postalCode"))
.setCity(getValOr(addressMap, "city"))
.setCountry(getValOr(addressMap, "country"))
.setLine1(getValOr(addressMap, "line1"))
.setLine2(getValOr(addressMap, "line2"))
.setState(getValOr(addressMap, "state"))
.build()

cardAddress?.let { ca ->
ca.postalCode?.let {
address.setPostalCode(it)
}
ca.country?.let {
address.setCountry(it)
}
}

return address.build()
}

internal fun mapToBillingDetails(billingDetails: ReadableMap?): PaymentMethod.BillingDetails? {
internal fun mapToBillingDetails(billingDetails: ReadableMap?, cardAddress: Address?): PaymentMethod.BillingDetails? {
if (billingDetails == null) {
return null
}
Expand All @@ -419,10 +429,18 @@ internal fun mapToBillingDetails(billingDetails: ReadableMap?): PaymentMethod.Bi
.setLine1(getValOr(billingDetails, "addressLine1"))
.setLine2(getValOr(billingDetails, "addressLine2"))
.setState(getValOr(billingDetails, "addressState"))
.build()

cardAddress?.let { ca ->
ca.postalCode?.let {
address.setPostalCode(it)
}
ca.country?.let {
address.setCountry(it)
}
}

return PaymentMethod.BillingDetails.Builder()
.setAddress(address)
.setAddress(address.build())
.setName(getValOr(billingDetails, "name"))
.setPhone(getValOr(billingDetails, "phone"))
.setEmail(getValOr(billingDetails, "email"))
Expand Down
Loading