Skip to content

Commit 3ac806e

Browse files
committed
feat: provide card-form state via PrimerCardFormProvider context
1 parent 37b9770 commit 3ac806e

19 files changed

Lines changed: 160 additions & 48 deletions

src/Components/PrimerCardForm.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,17 @@ import { PrimerExpiryDateInput } from './inputs/PrimerExpiryDateInput';
77
import { PrimerCVVInput } from './inputs/PrimerCVVInput';
88
import { PrimerCardholderNameInput } from './inputs/PrimerCardholderNameInput';
99
import { PrimerAcceptedCardNetworks } from './PrimerAcceptedCardNetworks';
10+
import { usePrimerCardForm } from './hooks/usePrimerCardForm';
1011
import type { PrimerTextInputRef } from './types/CardInputTypes';
1112
import type { PrimerCardFormProps } from './types/PrimerCardFormTypes';
1213

1314
export function PrimerCardForm({
14-
cardForm,
1515
onSubmit,
1616
autoFocus = false,
1717
style,
1818
testID = 'primer-card-form',
1919
}: PrimerCardFormProps) {
20+
const cardForm = usePrimerCardForm();
2021
const tokens = usePrimerTheme();
2122
const styles = useMemo(() => createStyles(tokens), [tokens]);
2223

@@ -40,7 +41,6 @@ export function PrimerCardForm({
4041
<PrimerAcceptedCardNetworks testID={`${testID}-accepted-networks`} />
4142
<PrimerCardNumberInput
4243
ref={cardRef}
43-
cardForm={cardForm}
4444
editable={!disabled}
4545
returnKeyType="next"
4646
onSubmitEditing={() => expiryRef.current?.focus()}
@@ -51,7 +51,6 @@ export function PrimerCardForm({
5151
<View style={styles.halfField}>
5252
<PrimerExpiryDateInput
5353
ref={expiryRef}
54-
cardForm={cardForm}
5554
editable={!disabled}
5655
returnKeyType="next"
5756
onSubmitEditing={() => cvvRef.current?.focus()}
@@ -61,7 +60,6 @@ export function PrimerCardForm({
6160
<View style={styles.halfField}>
6261
<PrimerCVVInput
6362
ref={cvvRef}
64-
cardForm={cardForm}
6563
editable={!disabled}
6664
returnKeyType={cardForm.isCardholderNameVisible ? 'next' : 'done'}
6765
onSubmitEditing={cardForm.isCardholderNameVisible ? () => nameRef.current?.focus() : onSubmit}
@@ -74,7 +72,6 @@ export function PrimerCardForm({
7472
{cardForm.isCardholderNameVisible && (
7573
<PrimerCardholderNameInput
7674
ref={nameRef}
77-
cardForm={cardForm}
7875
editable={!disabled}
7976
returnKeyType="done"
8077
onSubmitEditing={onSubmit}

src/Components/PrimerCheckoutProvider.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ interface InternalState {
5959
vaultedError: Error | null;
6060
activeVaultedMethodId: string | null;
6161
vaultDisplayOverride: 'expanded' | null;
62+
// Shopper-picked co-badged card network. Null until the user makes a
63+
// selection in the popover. Persists across re-renders / hook callers.
6264
selectedCardNetwork: CardNetworkId | null;
6365
requiresVaultedCardCvv: boolean;
6466
cvvInputVisible: boolean;

src/Components/hooks/usePrimerCardForm.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,19 @@ import { usePrimerCheckout } from './usePrimerCheckout';
44
import type { UseCardFormOptions, UsePrimerCardFormReturn } from '../types/CardFormTypes';
55

66
/**
7-
* Card form adapter hook. Thin wrapper over `CardFormStateProvider` — the
7+
* Card form adapter hook. Thin wrapper over `PrimerCardFormProvider` — the
88
* provider owns the field state, focus tracking, debouncer, and submit lifecycle.
99
* This hook reads the shared state and wires per-caller option callbacks
1010
* (`onValidationChange`, `onBinDataChange`, `onMetadataChange`).
1111
*
1212
* Must be used inside a `<PrimerCheckoutProvider>` tree that also mounts
13-
* `<CardFormStateProvider>` (the drop-in `CheckoutFlow` does this for you).
13+
* `<PrimerCardFormProvider>` (the drop-in `CheckoutFlow` does this for you).
1414
*/
1515
export function usePrimerCardForm(options: UseCardFormOptions = {}): UsePrimerCardFormReturn {
1616
const state = useCardFormStateContext();
1717
if (!state) {
1818
throw new Error(
19-
'usePrimerCardForm must be used within a <CardFormStateProvider>. ' +
19+
'usePrimerCardForm must be used within a <PrimerCardFormProvider>. ' +
2020
'The built-in <PrimerCheckoutSheet>/<CheckoutFlow> wires this automatically.'
2121
);
2222
}

src/Components/hooks/usePrimerCardNetworkSelection.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useCallback, useMemo } from 'react';
22
import { usePrimerCheckout } from './usePrimerCheckout';
33
import type { CardNetworkDetails, UsePrimerCardNetworkSelectionReturn } from '../types/CardNetworkSelection';
44
import type { CardNetworkId } from '../internal/cardNetwork';
5-
import type { PrimerCardNetwork } from '../../models/PrimerBinData';
5+
import type { PrimerCardNetwork as PrimerBinCardNetwork } from '../../models/PrimerBinData';
66

77
const LOG = '[usePrimerCardNetworkSelection]';
88

@@ -13,13 +13,13 @@ const NON_SELECTABLE_NETWORKS = new Set<string>(['EFTPOS']);
1313
// A card is co-badged when the BIN resolves to at least this many networks.
1414
const MIN_COBADGE_NETWORKS = 2;
1515

16-
function toCardNetwork(raw: PrimerCardNetwork): CardNetworkDetails {
16+
function toCardNetwork(raw: PrimerBinCardNetwork): CardNetworkDetails {
1717
// Native emits the closed set of CardNetworkId strings; cast is faithful to that contract.
1818
const identifier = raw.network as CardNetworkId;
1919
return {
2020
identifier,
2121
displayName: raw.displayName,
22-
// `allowed` is iOS-only on PrimerCardNetwork. undefined → treat as allowed.
22+
// `allowed` is iOS-only on PrimerBinCardNetwork. undefined → treat as allowed.
2323
logoUri: null,
2424
allowed: raw.allowed !== false,
2525
allowsUserSelection: !NON_SELECTABLE_NETWORKS.has(identifier),
@@ -34,7 +34,7 @@ export function usePrimerCardNetworkSelection(): UsePrimerCardNetworkSelectionRe
3434
if (!binData) {
3535
return [];
3636
}
37-
const all: PrimerCardNetwork[] = binData.preferred
37+
const all: PrimerBinCardNetwork[] = binData.preferred
3838
? [binData.preferred, ...binData.alternatives]
3939
: [...binData.alternatives];
4040
return all.map(toCardNetwork).filter((n) => n.allowed);

src/Components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export { PrimerCheckoutSheet } from './PrimerCheckoutSheet';
4545
export type { PrimerCheckoutSheetProps } from './PrimerCheckoutSheet';
4646
export { PrimerCardForm } from './PrimerCardForm';
4747
export type { PrimerCardFormProps } from './types/PrimerCardFormTypes';
48+
export { PrimerCardFormProvider } from './internal/form-state';
4849
export { PrimerAcceptedCardNetworks } from './PrimerAcceptedCardNetworks';
4950
export type { PrimerAcceptedCardNetworksProps } from './PrimerAcceptedCardNetworks';
5051
export { PrimerBillingAddressForm } from './PrimerBillingAddressForm';

src/Components/inputs/PrimerCVVInput.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@ import { Image, StyleSheet } from 'react-native';
33
import { PrimerTextInput } from './PrimerTextInput';
44
import { TRAILING_ICON_MARGIN, TRAILING_ICON_SIZE } from './dimensions';
55
import { usePrimerLocalization } from '../internal/localization';
6+
import { usePrimerCardForm } from '../hooks/usePrimerCardForm';
67
import type { PrimerCVVInputProps, PrimerTextInputRef } from '../types/CardInputTypes';
78

89
const cvvSource = require('../../assets/images/ic-cvv-hint.png');
910

1011
export const PrimerCVVInput = forwardRef<PrimerTextInputRef, PrimerCVVInputProps>(function PrimerCVVInput(
11-
{ cardForm, placeholder, label, ...rest },
12+
{ placeholder, label, ...rest },
1213
ref
1314
) {
15+
const cardForm = usePrimerCardForm();
1416
const { t } = usePrimerLocalization();
1517
const resolvedLabel = label ?? t('primer_card_form_label_cvv');
1618
const resolvedPlaceholder = placeholder ?? t('primer_card_form_placeholder_cvv');

src/Components/inputs/PrimerCardNumberInput.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { caretFromDigitIndex, countDigits, countDigitsBefore, targetDigitIndex }
55
import { PLACEHOLDER_ICON_HEIGHT, PLACEHOLDER_ICON_WIDTH, TRAILING_ICON_MARGIN } from './dimensions';
66
import { usePrimerLocalization } from '../internal/localization';
77
import { usePrimerTheme } from '../internal/theme';
8+
import { usePrimerCardForm } from '../hooks/usePrimerCardForm';
89
import { usePrimerCardNetwork } from '../hooks/usePrimerCardNetwork';
910
import { usePrimerCardNetworkSelection } from '../hooks/usePrimerCardNetworkSelection';
1011
import { PrimerCardNetworkSelector } from '../PrimerCardNetworkSelector';
@@ -16,7 +17,8 @@ type SelectionChangeHandler = NonNullable<TextInputProps['onSelectionChange']>;
1617
const placeholderSource = require('../../assets/images/ic-card-placeholder.png');
1718

1819
export const PrimerCardNumberInput = forwardRef<PrimerTextInputRef, PrimerCardNumberInputProps>(
19-
function PrimerCardNumberInput({ cardForm, placeholder, label, ...rest }, ref) {
20+
function PrimerCardNumberInput({ placeholder, label, ...rest }, ref) {
21+
const cardForm = usePrimerCardForm();
2022
const { t } = usePrimerLocalization();
2123
const resolvedLabel = label ?? t('primer_card_form_label_number');
2224
const resolvedPlaceholder = placeholder ?? t('primer_card_form_placeholder_number');

src/Components/inputs/PrimerCardholderNameInput.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { forwardRef } from 'react';
22
import { PrimerTextInput } from './PrimerTextInput';
33
import { usePrimerLocalization } from '../internal/localization';
4+
import { usePrimerCardForm } from '../hooks/usePrimerCardForm';
45
import type { PrimerCardholderNameInputProps, PrimerTextInputRef } from '../types/CardInputTypes';
56

67
export const PrimerCardholderNameInput = forwardRef<PrimerTextInputRef, PrimerCardholderNameInputProps>(
7-
function PrimerCardholderNameInput({ cardForm, placeholder, label, ...rest }, ref) {
8+
function PrimerCardholderNameInput({ placeholder, label, ...rest }, ref) {
9+
const cardForm = usePrimerCardForm();
810
const { t } = usePrimerLocalization();
911
const resolvedLabel = label ?? t('primer_card_form_label_name');
1012
const resolvedPlaceholder = placeholder ?? t('primer_card_form_placeholder_name');

src/Components/inputs/PrimerExpiryDateInput.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@ import { PrimerTextInput } from './PrimerTextInput';
44
import { caretFromDigitIndex, countDigits, countDigitsBefore, targetDigitIndex } from './caret';
55
import { TRAILING_ICON_MARGIN, TRAILING_ICON_SIZE } from './dimensions';
66
import { usePrimerLocalization } from '../internal/localization';
7+
import { usePrimerCardForm } from '../hooks/usePrimerCardForm';
78
import type { PrimerExpiryDateInputProps, PrimerTextInputRef } from '../types/CardInputTypes';
89

910
type SelectionChangeHandler = NonNullable<TextInputProps['onSelectionChange']>;
1011

1112
const calendarSource = require('../../assets/images/ic-expiry-calendar.png');
1213

1314
export const PrimerExpiryDateInput = forwardRef<PrimerTextInputRef, PrimerExpiryDateInputProps>(
14-
function PrimerExpiryDateInput({ cardForm, placeholder, label, ...rest }, ref) {
15+
function PrimerExpiryDateInput({ placeholder, label, ...rest }, ref) {
16+
const cardForm = usePrimerCardForm();
1517
const { t } = usePrimerLocalization();
1618
const resolvedLabel = label ?? t('primer_card_form_label_expiry');
1719
const resolvedPlaceholder = placeholder ?? t('primer_card_form_placeholder_expiry');

src/Components/internal/checkout-flow/CheckoutFlow.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { CountrySelectorScreen } from '../screens/CountrySelectorScreen';
1212
import { ErrorScreen } from '../screens/ErrorScreen';
1313
import { SuccessScreen } from '../screens/SuccessScreen';
1414
import { VaultedMethodsScreen } from '../screens/VaultedMethodsScreen';
15-
import { CardFormStateProvider, BillingAddressFormStateProvider } from '../form-state';
15+
import { PrimerCardFormProvider, BillingAddressFormStateProvider } from '../form-state';
1616
import { CheckoutFlowContext } from './CheckoutFlowContext';
1717

1818
const SUCCESS_AUTO_DISMISS_MS = 3000;
@@ -113,11 +113,11 @@ export function CheckoutFlow({ onCancel }: CheckoutFlowProps = {}) {
113113
<NavigationProvider initialRoute={CheckoutRoute.splash}>
114114
<ReadinessTransitioner />
115115
<PaymentOutcomeTransitioner />
116-
<CardFormStateProvider>
116+
<PrimerCardFormProvider>
117117
<BillingAddressFormStateProvider>
118118
<NavigationContainer screenMap={screenMap} />
119119
</BillingAddressFormStateProvider>
120-
</CardFormStateProvider>
120+
</PrimerCardFormProvider>
121121
</NavigationProvider>
122122
</CheckoutFlowContext.Provider>
123123
);

0 commit comments

Comments
 (0)