Skip to content

Commit 918bf9a

Browse files
committed
Migrate CBC to CardEditUIHandler
1 parent f0ac4f7 commit 918bf9a

17 files changed

+733
-668
lines changed

paymentsheet/src/main/java/com/stripe/android/customersheet/CustomerSheetViewModel.kt

+7-8
Original file line numberDiff line numberDiff line change
@@ -544,21 +544,14 @@ internal class CustomerSheetViewModel(
544544

545545
transition(
546546
to = CustomerSheetViewState.UpdatePaymentMethod(
547-
updatePaymentMethodInteractor = DefaultUpdatePaymentMethodInteractor(
547+
updatePaymentMethodInteractor = DefaultUpdatePaymentMethodInteractor.factory(
548548
isLiveMode = isLiveModeProvider(),
549549
canRemove = customerState.canRemove,
550550
displayableSavedPaymentMethod = paymentMethod,
551551
cardBrandFilter = PaymentSheetCardBrandFilter(customerState.configuration.cardBrandAcceptance),
552552
removeExecutor = ::removeExecutor,
553-
onBrandChoiceSelected = { brand ->
554-
eventReporter.onBrandChoiceSelected(
555-
source = CustomerSheetEventReporter.CardBrandChoiceEventSource.Edit,
556-
selectedBrand = brand
557-
)
558-
},
559553
onUpdateSuccess = ::onBackPressed,
560554
updatePaymentMethodExecutor = ::updatePaymentMethodExecutor,
561-
workContext = workContext,
562555
// This checkbox is never displayed in CustomerSheet.
563556
shouldShowSetAsDefaultCheckbox = false,
564557
isDefaultPaymentMethod = false,
@@ -568,6 +561,12 @@ internal class CustomerSheetViewModel(
568561
IllegalStateException("Unexpected attempt to update default from CustomerSheet.")
569562
)
570563
},
564+
onBrandChoiceSelected = { brand ->
565+
eventReporter.onBrandChoiceSelected(
566+
source = CustomerSheetEventReporter.CardBrandChoiceEventSource.Edit,
567+
selectedBrand = brand
568+
)
569+
}
571570
),
572571
isLiveMode = isLiveModeProvider(),
573572
)

paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/manage/EmbeddedUpdateScreenInteractorFactory.kt

+7-7
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ internal class DefaultEmbeddedUpdateScreenInteractorFactory @Inject constructor(
2929
override fun createUpdateScreenInteractor(
3030
displayableSavedPaymentMethod: DisplayableSavedPaymentMethod
3131
): UpdatePaymentMethodInteractor {
32-
return DefaultUpdatePaymentMethodInteractor(
32+
return DefaultUpdatePaymentMethodInteractor.factory(
3333
isLiveMode = paymentMethodMetadata.stripeIntent.isLiveMode,
3434
canRemove = customerStateHolder.canRemove.value,
3535
displayableSavedPaymentMethod = displayableSavedPaymentMethod,
@@ -59,12 +59,6 @@ internal class DefaultEmbeddedUpdateScreenInteractorFactory @Inject constructor(
5959
setDefaultPaymentMethodExecutor = { method ->
6060
savedPaymentMethodMutatorProvider.get().setDefaultPaymentMethod(method)
6161
},
62-
onBrandChoiceSelected = {
63-
eventReporter.onBrandChoiceSelected(
64-
source = EventReporter.CardBrandChoiceEventSource.Edit,
65-
selectedBrand = it
66-
)
67-
},
6862
shouldShowSetAsDefaultCheckbox = (
6963
paymentMethodMetadata.customerMetadata?.isPaymentMethodSetAsDefaultEnabled == true
7064
),
@@ -76,6 +70,12 @@ internal class DefaultEmbeddedUpdateScreenInteractorFactory @Inject constructor(
7670
onUpdateSuccess = {
7771
manageNavigatorProvider.get().performAction(ManageNavigator.Action.Back)
7872
},
73+
onBrandChoiceSelected = {
74+
eventReporter.onBrandChoiceSelected(
75+
source = EventReporter.CardBrandChoiceEventSource.Edit,
76+
selectedBrand = it
77+
)
78+
}
7979
)
8080
}
8181
}

paymentsheet/src/main/java/com/stripe/android/paymentsheet/SavedPaymentMethodMutator.kt

+8-8
Original file line numberDiff line numberDiff line change
@@ -361,10 +361,10 @@ internal class SavedPaymentMethodMutator(
361361
val isLiveMode = requireNotNull(viewModel.paymentMethodMetadata.value).stripeIntent.isLiveMode
362362
viewModel.navigationHandler.transitionTo(
363363
PaymentSheetScreen.UpdatePaymentMethod(
364-
DefaultUpdatePaymentMethodInteractor(
364+
DefaultUpdatePaymentMethodInteractor.factory(
365365
isLiveMode = isLiveMode,
366366
canRemove = canRemove,
367-
displayableSavedPaymentMethod,
367+
displayableSavedPaymentMethod = displayableSavedPaymentMethod,
368368
cardBrandFilter = PaymentSheetCardBrandFilter(viewModel.config.cardBrandAcceptance),
369369
removeExecutor = { method ->
370370
performRemove()
@@ -373,12 +373,6 @@ internal class SavedPaymentMethodMutator(
373373
updatePaymentMethodExecutor(cardUpdateParams)
374374
},
375375
setDefaultPaymentMethodExecutor = setDefaultPaymentMethodExecutor,
376-
onBrandChoiceSelected = {
377-
viewModel.eventReporter.onBrandChoiceSelected(
378-
source = EventReporter.CardBrandChoiceEventSource.Edit,
379-
selectedBrand = it
380-
)
381-
},
382376
shouldShowSetAsDefaultCheckbox = (
383377
viewModel
384378
.paymentMethodMetadata
@@ -391,6 +385,12 @@ internal class SavedPaymentMethodMutator(
391385
)
392386
),
393387
onUpdateSuccess = viewModel.navigationHandler::pop,
388+
onBrandChoiceSelected = {
389+
viewModel.eventReporter.onBrandChoiceSelected(
390+
source = EventReporter.CardBrandChoiceEventSource.Edit,
391+
selectedBrand = it
392+
)
393+
},
394394
)
395395
)
396396
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
package com.stripe.android.paymentsheet.ui
2+
3+
import android.content.res.Resources
4+
import androidx.compose.foundation.Image
5+
import androidx.compose.foundation.layout.Column
6+
import androidx.compose.foundation.layout.Row
7+
import androidx.compose.foundation.layout.fillMaxWidth
8+
import androidx.compose.foundation.layout.height
9+
import androidx.compose.foundation.layout.width
10+
import androidx.compose.foundation.shape.ZeroCornerSize
11+
import androidx.compose.material.Card
12+
import androidx.compose.material.Divider
13+
import androidx.compose.material.MaterialTheme
14+
import androidx.compose.runtime.Composable
15+
import androidx.compose.runtime.getValue
16+
import androidx.compose.runtime.mutableStateOf
17+
import androidx.compose.runtime.remember
18+
import androidx.compose.ui.Alignment
19+
import androidx.compose.ui.Modifier
20+
import androidx.compose.ui.layout.onSizeChanged
21+
import androidx.compose.ui.platform.testTag
22+
import androidx.compose.ui.res.painterResource
23+
import androidx.compose.ui.res.stringResource
24+
import androidx.compose.ui.unit.dp
25+
import com.stripe.android.CardBrandFilter
26+
import com.stripe.android.R
27+
import com.stripe.android.model.CardBrand
28+
import com.stripe.android.model.PaymentMethod
29+
import com.stripe.android.uicore.getBorderStroke
30+
import com.stripe.android.uicore.stripeColors
31+
import com.stripe.android.uicore.stripeShapes
32+
import com.stripe.android.uicore.utils.collectAsState
33+
34+
@Composable
35+
internal fun CardDetailsEditUI(
36+
cardEditUIHandler: CardEditUIHandler,
37+
isExpiredCard: Boolean,
38+
modifier: Modifier = Modifier
39+
) {
40+
val state by cardEditUIHandler.state.collectAsState()
41+
val dividerHeight = remember { mutableStateOf(0.dp) }
42+
43+
Card(
44+
border = MaterialTheme.getBorderStroke(false),
45+
elevation = 0.dp,
46+
modifier = modifier.testTag(UPDATE_PM_CARD_TEST_TAG),
47+
) {
48+
Column {
49+
CardNumberField(
50+
card = state.card,
51+
selectedBrand = state.selectedCardBrand,
52+
shouldShowCardBrandDropdown = cardEditUIHandler.showCardBrandDropdown,
53+
cardBrandFilter = cardEditUIHandler.cardBrandFilter,
54+
savedPaymentMethodIcon = cardEditUIHandler.paymentMethodIcon,
55+
onBrandChoiceChanged = {
56+
cardEditUIHandler.onBrandChoiceChanged(it)
57+
},
58+
)
59+
Divider(
60+
color = MaterialTheme.stripeColors.componentDivider,
61+
thickness = MaterialTheme.stripeShapes.borderStrokeWidth.dp,
62+
)
63+
Row(modifier = Modifier.fillMaxWidth()) {
64+
ExpiryField(
65+
expiryMonth = state.card.expiryMonth,
66+
expiryYear = state.card.expiryYear,
67+
isExpired = isExpiredCard,
68+
modifier = Modifier
69+
.weight(1F)
70+
.onSizeChanged {
71+
dividerHeight.value =
72+
(it.height / Resources.getSystem().displayMetrics.density).dp
73+
},
74+
)
75+
Divider(
76+
modifier = Modifier
77+
.height(dividerHeight.value)
78+
.width(MaterialTheme.stripeShapes.borderStrokeWidth.dp),
79+
color = MaterialTheme.stripeColors.componentDivider,
80+
)
81+
CvcField(cardBrand = state.card.brand, modifier = Modifier.weight(1F))
82+
}
83+
}
84+
}
85+
}
86+
87+
@Composable
88+
private fun CardNumberField(
89+
card: PaymentMethod.Card,
90+
selectedBrand: CardBrandChoice,
91+
cardBrandFilter: CardBrandFilter,
92+
shouldShowCardBrandDropdown: Boolean,
93+
savedPaymentMethodIcon: Int,
94+
onBrandChoiceChanged: (CardBrandChoice) -> Unit,
95+
) {
96+
CommonTextField(
97+
value = "•••• •••• •••• ${card.last4}",
98+
label = stringResource(id = R.string.stripe_acc_label_card_number),
99+
trailingIcon = {
100+
if (shouldShowCardBrandDropdown) {
101+
CardBrandDropdown(
102+
selectedBrand = selectedBrand,
103+
availableBrands = card.getAvailableNetworks(cardBrandFilter),
104+
onBrandChoiceChanged = onBrandChoiceChanged,
105+
)
106+
} else {
107+
PaymentMethodIconFromResource(
108+
iconRes = savedPaymentMethodIcon,
109+
colorFilter = null,
110+
alignment = Alignment.Center,
111+
modifier = Modifier,
112+
)
113+
}
114+
},
115+
)
116+
}
117+
118+
@Composable
119+
private fun ExpiryField(
120+
expiryMonth: Int?,
121+
expiryYear: Int?,
122+
isExpired: Boolean,
123+
modifier: Modifier
124+
) {
125+
CommonTextField(
126+
modifier = modifier.testTag(UPDATE_PM_EXPIRY_FIELD_TEST_TAG),
127+
value = formattedExpiryDate(expiryMonth = expiryMonth, expiryYear = expiryYear),
128+
label = stringResource(id = com.stripe.android.uicore.R.string.stripe_expiration_date_hint),
129+
shape = MaterialTheme.shapes.small.copy(
130+
topStart = ZeroCornerSize,
131+
topEnd = ZeroCornerSize,
132+
bottomEnd = ZeroCornerSize,
133+
),
134+
shouldShowError = isExpired,
135+
)
136+
}
137+
138+
private fun formattedExpiryDate(expiryMonth: Int?, expiryYear: Int?): String {
139+
@Suppress("ComplexCondition")
140+
if (
141+
expiryMonth == null ||
142+
monthIsInvalid(expiryMonth) ||
143+
expiryYear == null ||
144+
yearIsInvalid(expiryYear)
145+
) {
146+
return "••/••"
147+
}
148+
149+
val formattedExpiryMonth = if (expiryMonth < OCTOBER) {
150+
"0$expiryMonth"
151+
} else {
152+
expiryMonth.toString()
153+
}
154+
155+
@Suppress("MagicNumber")
156+
val formattedExpiryYear = expiryYear.toString().substring(2, 4)
157+
158+
return "$formattedExpiryMonth/$formattedExpiryYear"
159+
}
160+
161+
private fun monthIsInvalid(expiryMonth: Int): Boolean {
162+
return expiryMonth < JANUARY || expiryMonth > DECEMBER
163+
}
164+
165+
private fun yearIsInvalid(expiryYear: Int): Boolean {
166+
// Since we use 2-digit years to represent the expiration year, we should keep dates to
167+
// this century.
168+
return expiryYear < YEAR_2000 || expiryYear > YEAR_2100
169+
}
170+
171+
@Composable
172+
private fun CvcField(cardBrand: CardBrand, modifier: Modifier) {
173+
val cvc = buildString {
174+
repeat(cardBrand.maxCvcLength) {
175+
append("")
176+
}
177+
}
178+
CommonTextField(
179+
modifier = modifier.testTag(UPDATE_PM_CVC_FIELD_TEST_TAG),
180+
value = cvc,
181+
label = stringResource(id = R.string.stripe_cvc_number_hint),
182+
shape = MaterialTheme.shapes.small.copy(
183+
topStart = ZeroCornerSize,
184+
topEnd = ZeroCornerSize,
185+
bottomStart = ZeroCornerSize
186+
),
187+
trailingIcon = {
188+
Image(
189+
painter = painterResource(cardBrand.cvcIcon),
190+
contentDescription = null,
191+
)
192+
},
193+
)
194+
}
195+
196+
private const val JANUARY = 1
197+
private const val OCTOBER = 10
198+
private const val DECEMBER = 12
199+
private const val YEAR_2000 = 2000
200+
private const val YEAR_2100 = 2100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.stripe.android.paymentsheet.ui
2+
3+
import androidx.compose.foundation.layout.fillMaxWidth
4+
import androidx.compose.foundation.shape.ZeroCornerSize
5+
import androidx.compose.material.ContentAlpha
6+
import androidx.compose.material.MaterialTheme
7+
import androidx.compose.material.Text
8+
import androidx.compose.material.TextField
9+
import androidx.compose.runtime.Composable
10+
import androidx.compose.ui.Modifier
11+
import androidx.compose.ui.graphics.Shape
12+
import com.stripe.android.uicore.elements.TextFieldColors
13+
import com.stripe.android.uicore.stripeColors
14+
15+
@Composable
16+
internal fun CommonTextField(
17+
value: String,
18+
label: String,
19+
modifier: Modifier = Modifier,
20+
trailingIcon: @Composable (() -> Unit)? = null,
21+
shouldShowError: Boolean = false,
22+
shape: Shape =
23+
MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize),
24+
) {
25+
TextField(
26+
modifier = modifier.fillMaxWidth(),
27+
value = value,
28+
enabled = false,
29+
label = {
30+
Label(
31+
text = label,
32+
)
33+
},
34+
trailingIcon = trailingIcon,
35+
shape = shape,
36+
colors = TextFieldColors(
37+
shouldShowError = shouldShowError,
38+
),
39+
onValueChange = {},
40+
)
41+
}
42+
43+
@Composable
44+
private fun Label(
45+
text: String,
46+
) {
47+
Text(
48+
text = text,
49+
color = MaterialTheme.stripeColors.placeholderText.copy(alpha = ContentAlpha.disabled),
50+
style = MaterialTheme.typography.subtitle1
51+
)
52+
}

0 commit comments

Comments
 (0)