@stripe/stripe-react-native 0.9.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,173 @@
1
+ package com.reactnativestripesdk
2
+
3
+ import android.graphics.Color
4
+ import android.os.Bundle
5
+ import com.stripe.android.paymentsheet.PaymentSheet
6
+
7
+ fun PaymentSheetFragment.buildPaymentSheetAppearance(userParams: Bundle?): PaymentSheet.Appearance {
8
+ val colorParams = userParams?.getBundle(PaymentSheetAppearanceKeys.COLORS)
9
+ val lightColorParams = colorParams?.getBundle(PaymentSheetAppearanceKeys.LIGHT) ?: colorParams
10
+ val darkColorParams = colorParams?.getBundle(PaymentSheetAppearanceKeys.DARK) ?: colorParams
11
+
12
+ return PaymentSheet.Appearance(
13
+ typography = buildTypography(userParams?.getBundle(PaymentSheetAppearanceKeys.FONT)),
14
+ colorsLight = buildColors(lightColorParams, PaymentSheet.Colors.defaultLight),
15
+ colorsDark = buildColors(darkColorParams, PaymentSheet.Colors.defaultDark),
16
+ shapes = buildShapes(userParams?.getBundle(PaymentSheetAppearanceKeys.SHAPES)),
17
+ primaryButton = buildPrimaryButton(userParams?.getBundle(PaymentSheetAppearanceKeys.PRIMARY_BUTTON))
18
+ )
19
+ }
20
+
21
+ private fun PaymentSheetFragment.buildTypography(fontParams: Bundle?): PaymentSheet.Typography {
22
+ return PaymentSheet.Typography.default.copy(
23
+ sizeScaleFactor = getFloatOr(fontParams, PaymentSheetAppearanceKeys.SCALE, PaymentSheet.Typography.default.sizeScaleFactor),
24
+ fontResId = getFontResId(fontParams, PaymentSheetAppearanceKeys.FAMILY, PaymentSheet.Typography.default.fontResId)
25
+ )
26
+ }
27
+
28
+ @Throws(PaymentSheetAppearanceException::class)
29
+ private fun colorFromHexOrDefault(hexString: String?, default: Int): Int {
30
+ return hexString?.trim()?.replace("#","")?.let {
31
+ if (it.length == 6 || it.length == 8) {
32
+ return Color.parseColor("#$it")
33
+ } else throw PaymentSheetAppearanceException("Failed to set Payment Sheet appearance. Expected hex string of length 6 or 8, but received: $it")
34
+ } ?: run {
35
+ return default
36
+ }
37
+ }
38
+
39
+ private fun buildColors(colorParams: Bundle?, default: PaymentSheet.Colors): PaymentSheet.Colors {
40
+ if (colorParams == null) {
41
+ return default
42
+ }
43
+
44
+ return default.copy(
45
+ primary = colorFromHexOrDefault(colorParams.getString(PaymentSheetAppearanceKeys.PRIMARY), default.primary),
46
+ surface = colorFromHexOrDefault(colorParams.getString(PaymentSheetAppearanceKeys.BACKGROUND), default.surface),
47
+ component = colorFromHexOrDefault(colorParams.getString(PaymentSheetAppearanceKeys.COMPONENT_BACKGROUND), default.component),
48
+ componentBorder = colorFromHexOrDefault(colorParams.getString(PaymentSheetAppearanceKeys.COMPONENT_BORDER), default.componentBorder),
49
+ componentDivider = colorFromHexOrDefault(colorParams.getString(PaymentSheetAppearanceKeys.COMPONENT_DIVIDER), default.componentDivider),
50
+ onComponent = colorFromHexOrDefault(colorParams.getString(PaymentSheetAppearanceKeys.COMPONENT_TEXT), default.onComponent),
51
+ onSurface = colorFromHexOrDefault(colorParams.getString(PaymentSheetAppearanceKeys.PRIMARY_TEXT), default.onSurface),
52
+ subtitle = colorFromHexOrDefault(colorParams.getString(PaymentSheetAppearanceKeys.SECONDARY_TEXT), default.subtitle),
53
+ placeholderText = colorFromHexOrDefault(colorParams.getString(PaymentSheetAppearanceKeys.PLACEHOLDER_TEXT), default.placeholderText),
54
+ appBarIcon = colorFromHexOrDefault(colorParams.getString(PaymentSheetAppearanceKeys.ICON), default.appBarIcon),
55
+ error = colorFromHexOrDefault(colorParams.getString(PaymentSheetAppearanceKeys.ERROR), default.error),
56
+ )
57
+ }
58
+
59
+ private fun buildShapes(shapeParams: Bundle?): PaymentSheet.Shapes {
60
+ return PaymentSheet.Shapes.default.copy(
61
+ cornerRadiusDp = getFloatOr(shapeParams, PaymentSheetAppearanceKeys.BORDER_RADIUS, PaymentSheet.Shapes.default.cornerRadiusDp),
62
+ borderStrokeWidthDp = getFloatOr(shapeParams, PaymentSheetAppearanceKeys.BORDER_WIDTH, PaymentSheet.Shapes.default.borderStrokeWidthDp)
63
+ )
64
+ }
65
+
66
+ private fun PaymentSheetFragment.buildPrimaryButton(params: Bundle?): PaymentSheet.PrimaryButton {
67
+ if (params == null) {
68
+ return PaymentSheet.PrimaryButton()
69
+ }
70
+
71
+ val fontParams = params.getBundle(PaymentSheetAppearanceKeys.FONT) ?: Bundle.EMPTY
72
+ val shapeParams = params.getBundle(PaymentSheetAppearanceKeys.SHAPES) ?: Bundle.EMPTY
73
+ val colorParams = params.getBundle(PaymentSheetAppearanceKeys.COLORS) ?: Bundle.EMPTY
74
+ val lightColorParams = colorParams.getBundle(PaymentSheetAppearanceKeys.LIGHT) ?: colorParams
75
+ val darkColorParams = colorParams.getBundle(PaymentSheetAppearanceKeys.DARK) ?: colorParams
76
+
77
+ return PaymentSheet.PrimaryButton(
78
+ colorsLight = buildPrimaryButtonColors(lightColorParams, PaymentSheet.PrimaryButtonColors.defaultLight),
79
+ colorsDark = buildPrimaryButtonColors(darkColorParams, PaymentSheet.PrimaryButtonColors.defaultDark),
80
+ shape = PaymentSheet.PrimaryButtonShape(
81
+ cornerRadiusDp = getFloatOrNull(shapeParams, PaymentSheetAppearanceKeys.BORDER_RADIUS),
82
+ borderStrokeWidthDp = getFloatOrNull(shapeParams, PaymentSheetAppearanceKeys.BORDER_WIDTH),
83
+ ),
84
+ typography = PaymentSheet.PrimaryButtonTypography(
85
+ fontResId = getFontResId(fontParams, PaymentSheetAppearanceKeys.FAMILY, null)
86
+ )
87
+ )
88
+ }
89
+
90
+ @Throws(PaymentSheetAppearanceException::class)
91
+ private fun buildPrimaryButtonColors(colorParams: Bundle, default: PaymentSheet.PrimaryButtonColors): PaymentSheet.PrimaryButtonColors {
92
+ return PaymentSheet.PrimaryButtonColors(
93
+ background = colorParams.getString(PaymentSheetAppearanceKeys.BACKGROUND)?.trim()?.replace("#", "")?.let {
94
+ if (it.length == 6 || it.length == 8) {
95
+ Color.parseColor("#$it")
96
+ } else throw PaymentSheetAppearanceException("Failed to set Payment Sheet appearance. Expected hex string of length 6 or 8, but received: $it")
97
+ } ?: run {
98
+ null
99
+ },
100
+ onBackground = colorFromHexOrDefault(colorParams.getString(PaymentSheetAppearanceKeys.TEXT), default.onBackground),
101
+ border = colorFromHexOrDefault(colorParams.getString(PaymentSheetAppearanceKeys.BORDER), default.border),
102
+ )
103
+ }
104
+
105
+ private fun getFloatOr(bundle: Bundle?, key: String, defaultValue: Float): Float {
106
+ return if (bundle?.containsKey(key) == true) {
107
+ bundle.getFloat(key, bundle.getInt(key).toFloat())
108
+ } else {
109
+ defaultValue
110
+ }
111
+ }
112
+
113
+ private fun getFloatOrNull(bundle: Bundle?, key: String): Float? {
114
+ return if (bundle?.containsKey(key) == true) {
115
+ bundle.getFloat(key, bundle.getInt(key).toFloat())
116
+ } else {
117
+ null
118
+ }
119
+ }
120
+
121
+ @Throws(PaymentSheetAppearanceException::class)
122
+ private fun PaymentSheetFragment.getFontResId(bundle: Bundle?, key: String, defaultValue: Int?): Int? {
123
+ val fontErrorPrefix = "Encountered an error when setting a custom font:"
124
+ if (bundle?.containsKey(key) != true) {
125
+ return defaultValue
126
+ }
127
+
128
+ val fontFileName = bundle.getString(key)
129
+ ?: throw PaymentSheetAppearanceException("$fontErrorPrefix expected String for font.$key, but received null.")
130
+ if (Regex("[^a-z0-9]").containsMatchIn(fontFileName)) {
131
+ throw PaymentSheetAppearanceException(
132
+ "$fontErrorPrefix appearance.font.$key should only contain lowercase alphanumeric characters on Android, but received '$fontFileName'. This value must match the filename in android/app/src/main/res/font"
133
+ )
134
+ }
135
+
136
+ val id = resources.getIdentifier(fontFileName, "font", context?.packageName)
137
+ if (id == 0) {
138
+ throw PaymentSheetAppearanceException("$fontErrorPrefix Failed to find font: $fontFileName")
139
+ } else {
140
+ return id
141
+ }
142
+ }
143
+
144
+ private class PaymentSheetAppearanceKeys {
145
+ companion object {
146
+ const val COLORS = "colors"
147
+ const val LIGHT = "light"
148
+ const val DARK = "dark"
149
+ const val PRIMARY = "primary"
150
+ const val BACKGROUND = "background"
151
+ const val COMPONENT_BACKGROUND = "componentBackground"
152
+ const val COMPONENT_BORDER = "componentBorder"
153
+ const val COMPONENT_DIVIDER = "componentDivider"
154
+ const val COMPONENT_TEXT = "componentText"
155
+ const val PRIMARY_TEXT = "primaryText"
156
+ const val SECONDARY_TEXT = "secondaryText"
157
+ const val PLACEHOLDER_TEXT = "placeholderText"
158
+ const val ICON = "icon"
159
+ const val ERROR = "error"
160
+
161
+ const val FONT = "font"
162
+ const val FAMILY = "family"
163
+ const val SCALE = "scale"
164
+
165
+ const val SHAPES = "shapes"
166
+ const val BORDER_RADIUS = "borderRadius"
167
+ const val BORDER_WIDTH = "borderWidth"
168
+
169
+ const val PRIMARY_BUTTON = "primaryButton"
170
+ const val TEXT = "text"
171
+ const val BORDER = "border"
172
+ }
173
+ }
@@ -1,8 +1,6 @@
1
1
  package com.reactnativestripesdk
2
2
 
3
3
  import android.content.Context
4
- import android.content.Intent
5
- import android.content.res.ColorStateList
6
4
  import android.graphics.Bitmap
7
5
  import android.graphics.Canvas
8
6
  import android.graphics.Color
@@ -12,29 +10,37 @@ import android.view.LayoutInflater
12
10
  import android.view.View
13
11
  import android.view.ViewGroup
14
12
  import android.widget.FrameLayout
13
+ import androidx.appcompat.app.AppCompatActivity
15
14
  import androidx.appcompat.content.res.AppCompatResources
16
15
  import androidx.core.graphics.drawable.DrawableCompat
17
16
  import androidx.fragment.app.Fragment
18
- import androidx.localbroadcastmanager.content.LocalBroadcastManager
17
+ import com.facebook.react.bridge.Promise
18
+ import com.facebook.react.bridge.ReactApplicationContext
19
+ import com.facebook.react.bridge.WritableMap
20
+ import com.facebook.react.bridge.WritableNativeMap
19
21
  import com.stripe.android.paymentsheet.PaymentOptionCallback
20
22
  import com.stripe.android.paymentsheet.PaymentSheet
23
+ import com.stripe.android.paymentsheet.PaymentSheetResult
21
24
  import com.stripe.android.paymentsheet.PaymentSheetResultCallback
22
25
  import java.io.ByteArrayOutputStream
23
26
 
24
- class PaymentSheetFragment : Fragment() {
27
+ class PaymentSheetFragment(
28
+ private val context: ReactApplicationContext,
29
+ private val initPromise: Promise
30
+ ) : Fragment() {
25
31
  private var paymentSheet: PaymentSheet? = null
26
32
  private var flowController: PaymentSheet.FlowController? = null
27
33
  private var paymentIntentClientSecret: String? = null
28
34
  private var setupIntentClientSecret: String? = null
29
35
  private lateinit var paymentSheetConfiguration: PaymentSheet.Configuration
30
- private lateinit var localBroadcastManager: LocalBroadcastManager
36
+ private var confirmPromise: Promise? = null
37
+ private var presentPromise: Promise? = null
31
38
 
32
39
  override fun onCreateView(
33
40
  inflater: LayoutInflater,
34
41
  container: ViewGroup?,
35
42
  savedInstanceState: Bundle?
36
43
  ): View {
37
- localBroadcastManager = LocalBroadcastManager.getInstance(requireContext())
38
44
  return FrameLayout(requireActivity()).also {
39
45
  it.visibility = View.GONE
40
46
  }
@@ -50,34 +56,50 @@ class PaymentSheetFragment : Fragment() {
50
56
  val googlePayEnabled = arguments?.getBoolean("googlePay")
51
57
  val testEnv = arguments?.getBoolean("testEnv")
52
58
  val allowsDelayedPaymentMethods = arguments?.getBoolean("allowsDelayedPaymentMethods")
53
- val primaryButtonColorHexStr = arguments?.getString("primaryButtonColor").orEmpty()
54
59
  val billingDetailsBundle = arguments?.getBundle("defaultBillingDetails")
55
60
  paymentIntentClientSecret = arguments?.getString("paymentIntentClientSecret").orEmpty()
56
61
  setupIntentClientSecret = arguments?.getString("setupIntentClientSecret").orEmpty()
62
+ val appearance = try {
63
+ buildPaymentSheetAppearance(arguments?.getBundle("appearance"))
64
+ } catch (error: PaymentSheetAppearanceException) {
65
+ initPromise.resolve(createError(ErrorType.Failed.toString(), error))
66
+ return
67
+ }
57
68
 
58
69
  val paymentOptionCallback = PaymentOptionCallback { paymentOption ->
59
- val intent = Intent(ON_PAYMENT_OPTION_ACTION)
60
-
61
70
  if (paymentOption != null) {
62
71
  val bitmap = getBitmapFromVectorDrawable(context, paymentOption.drawableResourceId)
63
72
  val imageString = getBase64FromBitmap(bitmap)
64
-
65
- intent.putExtra("label", paymentOption.label)
66
- intent.putExtra("image", imageString)
73
+ val option: WritableMap = WritableNativeMap()
74
+ option.putString("label", paymentOption.label)
75
+ option.putString("image", imageString)
76
+ presentPromise?.resolve(createResult("paymentOption", option))
67
77
  }
68
- localBroadcastManager.sendBroadcast(intent)
78
+ presentPromise?.resolve(WritableNativeMap())
69
79
  }
70
80
 
71
81
  val paymentResultCallback = PaymentSheetResultCallback { paymentResult ->
72
- val intent = Intent(ON_PAYMENT_RESULT_ACTION)
73
-
74
- intent.putExtra("paymentResult", paymentResult)
75
- localBroadcastManager.sendBroadcast(intent)
76
- }
77
-
78
- var primaryButtonColor: ColorStateList? = null
79
- if (primaryButtonColorHexStr.isNotEmpty()) {
80
- primaryButtonColor = ColorStateList.valueOf(Color.parseColor(primaryButtonColorHexStr))
82
+ when (paymentResult) {
83
+ is PaymentSheetResult.Canceled -> {
84
+ val message = "The payment has been canceled"
85
+ confirmPromise?.resolve(createError(PaymentSheetErrorType.Canceled.toString(), message))
86
+ ?: run {
87
+ presentPromise?.resolve(createError(PaymentSheetErrorType.Canceled.toString(), message))
88
+ }
89
+ }
90
+ is PaymentSheetResult.Failed -> {
91
+ confirmPromise?.resolve(createError(PaymentSheetErrorType.Failed.toString(), paymentResult.error))
92
+ ?: run {
93
+ presentPromise?.resolve(createError(PaymentSheetErrorType.Failed.toString(), paymentResult.error))
94
+ }
95
+ }
96
+ is PaymentSheetResult.Completed -> {
97
+ confirmPromise?.resolve(WritableNativeMap()) ?: run {
98
+ presentPromise?.resolve(WritableNativeMap())
99
+ }
100
+ }
101
+ }
102
+ (context.currentActivity as? AppCompatActivity)?.supportFragmentManager?.beginTransaction()?.remove(this)?.commitAllowingStateLoss()
81
103
  }
82
104
 
83
105
  var defaultBillingDetails: PaymentSheet.BillingDetails? = null
@@ -100,7 +122,6 @@ class PaymentSheetFragment : Fragment() {
100
122
  paymentSheetConfiguration = PaymentSheet.Configuration(
101
123
  merchantDisplayName = merchantDisplayName,
102
124
  allowsDelayedPaymentMethods = allowsDelayedPaymentMethods ?: false,
103
- primaryButtonColor = primaryButtonColor,
104
125
  defaultBillingDetails=defaultBillingDetails,
105
126
  customer = if (customerId.isNotEmpty() && customerEphemeralKeySecret.isNotEmpty()) PaymentSheet.CustomerConfiguration(
106
127
  id = customerId,
@@ -110,7 +131,8 @@ class PaymentSheetFragment : Fragment() {
110
131
  environment = if (testEnv == true) PaymentSheet.GooglePayConfiguration.Environment.Test else PaymentSheet.GooglePayConfiguration.Environment.Production,
111
132
  countryCode = countryCode,
112
133
  currencyCode = currencyCode
113
- ) else null
134
+ ) else null,
135
+ appearance = appearance
114
136
  )
115
137
 
116
138
  if (arguments?.getBoolean("customFlow") == true) {
@@ -118,12 +140,12 @@ class PaymentSheetFragment : Fragment() {
118
140
  configureFlowController()
119
141
  } else {
120
142
  paymentSheet = PaymentSheet(this, paymentResultCallback)
121
- val intent = Intent(ON_INIT_PAYMENT_SHEET)
122
- localBroadcastManager.sendBroadcast(intent)
143
+ initPromise.resolve(WritableNativeMap())
123
144
  }
124
145
  }
125
146
 
126
- fun present() {
147
+ fun present(promise: Promise) {
148
+ this.presentPromise = promise
127
149
  if(paymentSheet != null) {
128
150
  if (!paymentIntentClientSecret.isNullOrEmpty()) {
129
151
  paymentSheet?.presentWithPaymentIntent(paymentIntentClientSecret!!, paymentSheetConfiguration)
@@ -135,23 +157,24 @@ class PaymentSheetFragment : Fragment() {
135
157
  }
136
158
  }
137
159
 
138
- fun confirmPayment() {
160
+ fun confirmPayment(promise: Promise) {
161
+ this.confirmPromise = promise
139
162
  flowController?.confirm()
140
163
  }
141
164
 
142
165
  private fun configureFlowController() {
143
166
  val onFlowControllerConfigure = PaymentSheet.FlowController.ConfigCallback { _, _ ->
144
- val paymentOption = flowController?.getPaymentOption()
145
- val intent = Intent(ON_CONFIGURE_FLOW_CONTROLLER)
146
-
147
- paymentOption?.let {
167
+ val result = flowController?.getPaymentOption()?.let {
148
168
  val bitmap = getBitmapFromVectorDrawable(context, it.drawableResourceId)
149
169
  val imageString = getBase64FromBitmap(bitmap)
150
-
151
- intent.putExtra("label", it.label)
152
- intent.putExtra("image", imageString)
170
+ val option: WritableMap = WritableNativeMap()
171
+ option.putString("label", it.label)
172
+ option.putString("image", imageString)
173
+ createResult("paymentOption", option)
174
+ } ?: run {
175
+ WritableNativeMap()
153
176
  }
154
- localBroadcastManager.sendBroadcast(intent)
177
+ initPromise.resolve(result)
155
178
  }
156
179
 
157
180
  if (!paymentIntentClientSecret.isNullOrEmpty()) {