@whop/elements-react-native 0.1.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.
- package/README.md +179 -0
- package/android/build.gradle +64 -0
- package/android/consumer-rules.pro +3 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/java/com/whop/elements/WhopAddressSuggestionsModule.kt +102 -0
- package/android/src/main/java/com/whop/elements/WhopAuthSessionModule.kt +126 -0
- package/android/src/main/java/com/whop/elements/WhopElementsPackage.kt +39 -0
- package/android/src/main/java/com/whop/elements/WhopPayButtonManager.kt +74 -0
- package/android/src/main/java/com/whop/elements/WhopPayButtonPressEvent.kt +17 -0
- package/android/src/main/java/com/whop/elements/WhopPayButtonView.kt +178 -0
- package/android/src/main/java/com/whop/elements/WhopPlatformPayModule.kt +212 -0
- package/assets/whop-wordmark-dark.png +0 -0
- package/assets/whop-wordmark-dark@2x.png +0 -0
- package/assets/whop-wordmark-dark@3x.png +0 -0
- package/assets/whop-wordmark-light.png +0 -0
- package/assets/whop-wordmark-light@2x.png +0 -0
- package/assets/whop-wordmark-light@3x.png +0 -0
- package/ios/Package.swift +59 -0
- package/ios/Sources/ElementsReactNative/WhopAddressSuggestionsImpl.swift +189 -0
- package/ios/Sources/ElementsReactNative/WhopAuthSessionImpl.swift +106 -0
- package/ios/Sources/ElementsReactNative/WhopPayButtonImpl.swift +89 -0
- package/ios/Sources/ElementsReactNative/WhopPlatformPayImpl.swift +181 -0
- package/ios/generated/WhopAddressSuggestionsModule.h +11 -0
- package/ios/generated/WhopAddressSuggestionsModule.mm +46 -0
- package/ios/generated/WhopAuthSessionModule.h +11 -0
- package/ios/generated/WhopAuthSessionModule.mm +45 -0
- package/ios/generated/WhopElementsShims.h +35 -0
- package/ios/generated/WhopPayButtonComponentView.h +12 -0
- package/ios/generated/WhopPayButtonComponentView.mm +82 -0
- package/ios/generated/WhopPlatformPayModule.h +11 -0
- package/ios/generated/WhopPlatformPayModule.mm +51 -0
- package/lib/module/api/client.js +139 -0
- package/lib/module/api/client.js.map +1 -0
- package/lib/module/api/client.test.js +137 -0
- package/lib/module/api/client.test.js.map +1 -0
- package/lib/module/api/types.js +2 -0
- package/lib/module/api/types.js.map +1 -0
- package/lib/module/controller/actions.js +148 -0
- package/lib/module/controller/actions.js.map +1 -0
- package/lib/module/controller/actions.test.js +102 -0
- package/lib/module/controller/actions.test.js.map +1 -0
- package/lib/module/controller/store.js +158 -0
- package/lib/module/controller/store.js.map +1 -0
- package/lib/module/core/address/country-data.js +1370 -0
- package/lib/module/core/address/country-data.js.map +1 -0
- package/lib/module/core/address/country-name.js +38 -0
- package/lib/module/core/address/country-name.js.map +1 -0
- package/lib/module/core/address/country-names.generated.js +249 -0
- package/lib/module/core/address/country-names.generated.js.map +1 -0
- package/lib/module/core/address/engine.js +245 -0
- package/lib/module/core/address/engine.js.map +1 -0
- package/lib/module/core/address/engine.test.js +216 -0
- package/lib/module/core/address/engine.test.js.map +1 -0
- package/lib/module/core/address/form-state.js +409 -0
- package/lib/module/core/address/form-state.js.map +1 -0
- package/lib/module/core/address/form-state.test.js +516 -0
- package/lib/module/core/address/form-state.test.js.map +1 -0
- package/lib/module/core/address/places.js +36 -0
- package/lib/module/core/address/places.js.map +1 -0
- package/lib/module/core/address/required-coverage.test.js +57 -0
- package/lib/module/core/address/required-coverage.test.js.map +1 -0
- package/lib/module/core/address/suggest.js +102 -0
- package/lib/module/core/address/suggest.js.map +1 -0
- package/lib/module/core/address/suggest.test.js +156 -0
- package/lib/module/core/address/suggest.test.js.map +1 -0
- package/lib/module/core/address/use-suggestions.js +33 -0
- package/lib/module/core/address/use-suggestions.js.map +1 -0
- package/lib/module/core/payments/address-change.js +91 -0
- package/lib/module/core/payments/address-change.js.map +1 -0
- package/lib/module/core/payments/billing-merge.js +62 -0
- package/lib/module/core/payments/billing-merge.js.map +1 -0
- package/lib/module/core/payments/browser-info.js +42 -0
- package/lib/module/core/payments/browser-info.js.map +1 -0
- package/lib/module/core/payments/bt.js +151 -0
- package/lib/module/core/payments/bt.js.map +1 -0
- package/lib/module/core/payments/bt.test.js +23 -0
- package/lib/module/core/payments/bt.test.js.map +1 -0
- package/lib/module/core/payments/currency.js +37 -0
- package/lib/module/core/payments/currency.js.map +1 -0
- package/lib/module/core/payments/detail.js +159 -0
- package/lib/module/core/payments/detail.js.map +1 -0
- package/lib/module/core/payments/detail.test.js +225 -0
- package/lib/module/core/payments/detail.test.js.map +1 -0
- package/lib/module/core/payments/errors.js +115 -0
- package/lib/module/core/payments/errors.js.map +1 -0
- package/lib/module/core/payments/mint.js +89 -0
- package/lib/module/core/payments/mint.js.map +1 -0
- package/lib/module/core/payments/plan.js +131 -0
- package/lib/module/core/payments/plan.js.map +1 -0
- package/lib/module/core/payments/refusals.js +17 -0
- package/lib/module/core/payments/refusals.js.map +1 -0
- package/lib/module/core/payments/secure-fields.js +87 -0
- package/lib/module/core/payments/secure-fields.js.map +1 -0
- package/lib/module/core/payments/tax-id-catalog.js +616 -0
- package/lib/module/core/payments/tax-id-catalog.js.map +1 -0
- package/lib/module/core/payments/wallet-contact.js +42 -0
- package/lib/module/core/payments/wallet-contact.js.map +1 -0
- package/lib/module/core/payments/wallet-contact.test.js +77 -0
- package/lib/module/core/payments/wallet-contact.test.js.map +1 -0
- package/lib/module/elements/address.js +467 -0
- package/lib/module/elements/address.js.map +1 -0
- package/lib/module/elements/branding.js +114 -0
- package/lib/module/elements/branding.js.map +1 -0
- package/lib/module/elements/card-fields/context.js +254 -0
- package/lib/module/elements/card-fields/context.js.map +1 -0
- package/lib/module/elements/card-fields/fields.js +309 -0
- package/lib/module/elements/card-fields/fields.js.map +1 -0
- package/lib/module/elements/card-fields/index.js +30 -0
- package/lib/module/elements/card-fields/index.js.map +1 -0
- package/lib/module/elements/card.js +107 -0
- package/lib/module/elements/card.js.map +1 -0
- package/lib/module/elements/email.js +84 -0
- package/lib/module/elements/email.js.map +1 -0
- package/lib/module/elements/payment/declared-fields.js +186 -0
- package/lib/module/elements/payment/declared-fields.js.map +1 -0
- package/lib/module/elements/payment/index.js +448 -0
- package/lib/module/elements/payment/index.js.map +1 -0
- package/lib/module/elements/payment/wallet.js +132 -0
- package/lib/module/elements/payment/wallet.js.map +1 -0
- package/lib/module/elements/tax-id.js +100 -0
- package/lib/module/elements/tax-id.js.map +1 -0
- package/lib/module/i18n/catalogs.js +103 -0
- package/lib/module/i18n/catalogs.js.map +1 -0
- package/lib/module/i18n/index.js +45 -0
- package/lib/module/i18n/index.js.map +1 -0
- package/lib/module/index.js +27 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/namespace.js +174 -0
- package/lib/module/namespace.js.map +1 -0
- package/lib/module/native/index.js +128 -0
- package/lib/module/native/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/provider.js +56 -0
- package/lib/module/provider.js.map +1 -0
- package/lib/module/specs/NativeWhopAddressSuggestions.js +6 -0
- package/lib/module/specs/NativeWhopAddressSuggestions.js.map +1 -0
- package/lib/module/specs/NativeWhopAuthSession.js +10 -0
- package/lib/module/specs/NativeWhopAuthSession.js.map +1 -0
- package/lib/module/specs/NativeWhopPlatformPay.js +10 -0
- package/lib/module/specs/NativeWhopPlatformPay.js.map +1 -0
- package/lib/module/specs/WhopPayButtonNativeComponent.ts +22 -0
- package/lib/module/theme/appearance.js +85 -0
- package/lib/module/theme/appearance.js.map +1 -0
- package/lib/module/theme/appearance.test.js +124 -0
- package/lib/module/theme/appearance.test.js.map +1 -0
- package/lib/module/theme/provider.js +25 -0
- package/lib/module/theme/provider.js.map +1 -0
- package/lib/module/theme/resolve.js +69 -0
- package/lib/module/theme/resolve.js.map +1 -0
- package/lib/module/theme/scales.generated.js +419 -0
- package/lib/module/theme/scales.generated.js.map +1 -0
- package/lib/module/ui/element-frame.js +83 -0
- package/lib/module/ui/element-frame.js.map +1 -0
- package/lib/module/ui/payment-method-icon.js +69 -0
- package/lib/module/ui/payment-method-icon.js.map +1 -0
- package/lib/module/ui/primitives.js +266 -0
- package/lib/module/ui/primitives.js.map +1 -0
- package/lib/module/ui/select.js +281 -0
- package/lib/module/ui/select.js.map +1 -0
- package/lib/module/ui/suggestions.js +71 -0
- package/lib/module/ui/suggestions.js.map +1 -0
- package/lib/module/ui/whop-logo.js +42 -0
- package/lib/module/ui/whop-logo.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/api/client.d.ts +87 -0
- package/lib/typescript/src/api/client.d.ts.map +1 -0
- package/lib/typescript/src/api/types.d.ts +175 -0
- package/lib/typescript/src/api/types.d.ts.map +1 -0
- package/lib/typescript/src/controller/actions.d.ts +42 -0
- package/lib/typescript/src/controller/actions.d.ts.map +1 -0
- package/lib/typescript/src/controller/store.d.ts +90 -0
- package/lib/typescript/src/controller/store.d.ts.map +1 -0
- package/lib/typescript/src/core/address/country-data.d.ts +4 -0
- package/lib/typescript/src/core/address/country-data.d.ts.map +1 -0
- package/lib/typescript/src/core/address/country-name.d.ts +2 -0
- package/lib/typescript/src/core/address/country-name.d.ts.map +1 -0
- package/lib/typescript/src/core/address/country-names.generated.d.ts +2 -0
- package/lib/typescript/src/core/address/country-names.generated.d.ts.map +1 -0
- package/lib/typescript/src/core/address/engine.d.ts +97 -0
- package/lib/typescript/src/core/address/engine.d.ts.map +1 -0
- package/lib/typescript/src/core/address/form-state.d.ts +144 -0
- package/lib/typescript/src/core/address/form-state.d.ts.map +1 -0
- package/lib/typescript/src/core/address/places.d.ts +23 -0
- package/lib/typescript/src/core/address/places.d.ts.map +1 -0
- package/lib/typescript/src/core/address/suggest.d.ts +47 -0
- package/lib/typescript/src/core/address/suggest.d.ts.map +1 -0
- package/lib/typescript/src/core/address/use-suggestions.d.ts +3 -0
- package/lib/typescript/src/core/address/use-suggestions.d.ts.map +1 -0
- package/lib/typescript/src/core/payments/address-change.d.ts +52 -0
- package/lib/typescript/src/core/payments/address-change.d.ts.map +1 -0
- package/lib/typescript/src/core/payments/billing-merge.d.ts +39 -0
- package/lib/typescript/src/core/payments/billing-merge.d.ts.map +1 -0
- package/lib/typescript/src/core/payments/browser-info.d.ts +12 -0
- package/lib/typescript/src/core/payments/browser-info.d.ts.map +1 -0
- package/lib/typescript/src/core/payments/bt.d.ts +55 -0
- package/lib/typescript/src/core/payments/bt.d.ts.map +1 -0
- package/lib/typescript/src/core/payments/currency.d.ts +7 -0
- package/lib/typescript/src/core/payments/currency.d.ts.map +1 -0
- package/lib/typescript/src/core/payments/detail.d.ts +53 -0
- package/lib/typescript/src/core/payments/detail.d.ts.map +1 -0
- package/lib/typescript/src/core/payments/errors.d.ts +62 -0
- package/lib/typescript/src/core/payments/errors.d.ts.map +1 -0
- package/lib/typescript/src/core/payments/mint.d.ts +74 -0
- package/lib/typescript/src/core/payments/mint.d.ts.map +1 -0
- package/lib/typescript/src/core/payments/plan.d.ts +69 -0
- package/lib/typescript/src/core/payments/plan.d.ts.map +1 -0
- package/lib/typescript/src/core/payments/refusals.d.ts +12 -0
- package/lib/typescript/src/core/payments/refusals.d.ts.map +1 -0
- package/lib/typescript/src/core/payments/secure-fields.d.ts +33 -0
- package/lib/typescript/src/core/payments/secure-fields.d.ts.map +1 -0
- package/lib/typescript/src/core/payments/tax-id-catalog.d.ts +481 -0
- package/lib/typescript/src/core/payments/tax-id-catalog.d.ts.map +1 -0
- package/lib/typescript/src/core/payments/wallet-contact.d.ts +40 -0
- package/lib/typescript/src/core/payments/wallet-contact.d.ts.map +1 -0
- package/lib/typescript/src/elements/address.d.ts +55 -0
- package/lib/typescript/src/elements/address.d.ts.map +1 -0
- package/lib/typescript/src/elements/branding.d.ts +18 -0
- package/lib/typescript/src/elements/branding.d.ts.map +1 -0
- package/lib/typescript/src/elements/card-fields/context.d.ts +67 -0
- package/lib/typescript/src/elements/card-fields/context.d.ts.map +1 -0
- package/lib/typescript/src/elements/card-fields/fields.d.ts +15 -0
- package/lib/typescript/src/elements/card-fields/fields.d.ts.map +1 -0
- package/lib/typescript/src/elements/card-fields/index.d.ts +21 -0
- package/lib/typescript/src/elements/card-fields/index.d.ts.map +1 -0
- package/lib/typescript/src/elements/card.d.ts +21 -0
- package/lib/typescript/src/elements/card.d.ts.map +1 -0
- package/lib/typescript/src/elements/email.d.ts +31 -0
- package/lib/typescript/src/elements/email.d.ts.map +1 -0
- package/lib/typescript/src/elements/payment/declared-fields.d.ts +43 -0
- package/lib/typescript/src/elements/payment/declared-fields.d.ts.map +1 -0
- package/lib/typescript/src/elements/payment/index.d.ts +37 -0
- package/lib/typescript/src/elements/payment/index.d.ts.map +1 -0
- package/lib/typescript/src/elements/payment/wallet.d.ts +39 -0
- package/lib/typescript/src/elements/payment/wallet.d.ts.map +1 -0
- package/lib/typescript/src/elements/tax-id.d.ts +41 -0
- package/lib/typescript/src/elements/tax-id.d.ts.map +1 -0
- package/lib/typescript/src/i18n/catalogs.d.ts +7 -0
- package/lib/typescript/src/i18n/catalogs.d.ts.map +1 -0
- package/lib/typescript/src/i18n/index.d.ts +17 -0
- package/lib/typescript/src/i18n/index.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +42 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/namespace.d.ts +36 -0
- package/lib/typescript/src/namespace.d.ts.map +1 -0
- package/lib/typescript/src/native/index.d.ts +62 -0
- package/lib/typescript/src/native/index.d.ts.map +1 -0
- package/lib/typescript/src/provider.d.ts +37 -0
- package/lib/typescript/src/provider.d.ts.map +1 -0
- package/lib/typescript/src/specs/NativeWhopAddressSuggestions.d.ts +13 -0
- package/lib/typescript/src/specs/NativeWhopAddressSuggestions.d.ts.map +1 -0
- package/lib/typescript/src/specs/NativeWhopAuthSession.d.ts +20 -0
- package/lib/typescript/src/specs/NativeWhopAuthSession.d.ts.map +1 -0
- package/lib/typescript/src/specs/NativeWhopPlatformPay.d.ts +24 -0
- package/lib/typescript/src/specs/NativeWhopPlatformPay.d.ts.map +1 -0
- package/lib/typescript/src/specs/WhopPayButtonNativeComponent.d.ts +21 -0
- package/lib/typescript/src/specs/WhopPayButtonNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/theme/appearance.d.ts +71 -0
- package/lib/typescript/src/theme/appearance.d.ts.map +1 -0
- package/lib/typescript/src/theme/provider.d.ts +11 -0
- package/lib/typescript/src/theme/provider.d.ts.map +1 -0
- package/lib/typescript/src/theme/resolve.d.ts +19 -0
- package/lib/typescript/src/theme/resolve.d.ts.map +1 -0
- package/lib/typescript/src/theme/scales.generated.d.ts +18 -0
- package/lib/typescript/src/theme/scales.generated.d.ts.map +1 -0
- package/lib/typescript/src/ui/element-frame.d.ts +32 -0
- package/lib/typescript/src/ui/element-frame.d.ts.map +1 -0
- package/lib/typescript/src/ui/payment-method-icon.d.ts +21 -0
- package/lib/typescript/src/ui/payment-method-icon.d.ts.map +1 -0
- package/lib/typescript/src/ui/primitives.d.ts +94 -0
- package/lib/typescript/src/ui/primitives.d.ts.map +1 -0
- package/lib/typescript/src/ui/select.d.ts +25 -0
- package/lib/typescript/src/ui/select.d.ts.map +1 -0
- package/lib/typescript/src/ui/suggestions.d.ts +15 -0
- package/lib/typescript/src/ui/suggestions.d.ts.map +1 -0
- package/lib/typescript/src/ui/whop-logo.d.ts +4 -0
- package/lib/typescript/src/ui/whop-logo.d.ts.map +1 -0
- package/package.json +121 -0
- package/scripts/check-swift.sh +27 -0
- package/scripts/codegen-shims.mjs +409 -0
- package/scripts/gen-country-names.mjs +58 -0
- package/scripts/gen-theme.mjs +124 -0
- package/scripts/gen-wordmark.mjs +68 -0
- package/scripts/mirror.mjs +112 -0
- package/src/api/client.ts +202 -0
- package/src/api/types.ts +166 -0
- package/src/controller/actions.ts +197 -0
- package/src/controller/store.ts +201 -0
- package/src/core/address/country-data.ts +927 -0
- package/src/core/address/country-name.ts +36 -0
- package/src/core/address/country-names.generated.ts +246 -0
- package/src/core/address/engine.ts +349 -0
- package/src/core/address/form-state.ts +479 -0
- package/src/core/address/places.ts +39 -0
- package/src/core/address/suggest.ts +153 -0
- package/src/core/address/use-suggestions.ts +37 -0
- package/src/core/payments/address-change.ts +121 -0
- package/src/core/payments/billing-merge.ts +91 -0
- package/src/core/payments/browser-info.ts +43 -0
- package/src/core/payments/bt.ts +176 -0
- package/src/core/payments/currency.ts +54 -0
- package/src/core/payments/detail.ts +204 -0
- package/src/core/payments/errors.ts +171 -0
- package/src/core/payments/mint.ts +130 -0
- package/src/core/payments/plan.ts +198 -0
- package/src/core/payments/refusals.ts +24 -0
- package/src/core/payments/secure-fields.ts +85 -0
- package/src/core/payments/tax-id-catalog.ts +320 -0
- package/src/core/payments/wallet-contact.ts +56 -0
- package/src/elements/address.tsx +570 -0
- package/src/elements/branding.tsx +87 -0
- package/src/elements/card-fields/context.tsx +310 -0
- package/src/elements/card-fields/fields.tsx +281 -0
- package/src/elements/card-fields/index.tsx +37 -0
- package/src/elements/card.tsx +101 -0
- package/src/elements/email.tsx +100 -0
- package/src/elements/payment/declared-fields.tsx +238 -0
- package/src/elements/payment/index.tsx +497 -0
- package/src/elements/payment/wallet.tsx +155 -0
- package/src/elements/tax-id.tsx +143 -0
- package/src/i18n/catalogs.ts +100 -0
- package/src/i18n/index.tsx +46 -0
- package/src/index.ts +68 -0
- package/src/namespace.tsx +231 -0
- package/src/native/index.ts +173 -0
- package/src/provider.tsx +76 -0
- package/src/specs/NativeWhopAddressSuggestions.ts +14 -0
- package/src/specs/NativeWhopAuthSession.ts +21 -0
- package/src/specs/NativeWhopPlatformPay.ts +25 -0
- package/src/specs/WhopPayButtonNativeComponent.ts +22 -0
- package/src/theme/appearance.ts +150 -0
- package/src/theme/provider.tsx +28 -0
- package/src/theme/resolve.ts +68 -0
- package/src/theme/scales.generated.ts +427 -0
- package/src/ui/element-frame.tsx +94 -0
- package/src/ui/payment-method-icon.tsx +55 -0
- package/src/ui/primitives.tsx +269 -0
- package/src/ui/select.tsx +255 -0
- package/src/ui/suggestions.tsx +69 -0
- package/src/ui/whop-logo.tsx +37 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** The contact fields an OS wallet sheet collected, in the wire keys the mint sends. */
|
|
2
|
+
export interface WalletContact {
|
|
3
|
+
email?: string;
|
|
4
|
+
name?: string;
|
|
5
|
+
address?: {
|
|
6
|
+
line1?: string;
|
|
7
|
+
line2?: string;
|
|
8
|
+
city?: string;
|
|
9
|
+
state?: string;
|
|
10
|
+
postal_code?: string;
|
|
11
|
+
country?: string;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The native envelope, read tolerantly.
|
|
17
|
+
*
|
|
18
|
+
* Both platform modules answer with `{ paymentData, email?, name?, address? }`. Anything else is
|
|
19
|
+
* treated as the bare token, so an app whose native side has not been rebuilt still tokenizes — it
|
|
20
|
+
* just gets no contacts, which is exactly the behaviour it had before.
|
|
21
|
+
*/
|
|
22
|
+
export function readSheetResult(raw: string): { paymentData: string } & WalletContact {
|
|
23
|
+
try {
|
|
24
|
+
const parsed: unknown = JSON.parse(raw);
|
|
25
|
+
if (
|
|
26
|
+
parsed !== null &&
|
|
27
|
+
typeof parsed === 'object' &&
|
|
28
|
+
typeof (parsed as { paymentData?: unknown }).paymentData === 'string'
|
|
29
|
+
) {
|
|
30
|
+
return parsed as { paymentData: string } & WalletContact;
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
// Not JSON at all: Google Pay's opaque gateway token.
|
|
34
|
+
}
|
|
35
|
+
return { paymentData: raw };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Folds the sheet's own contacts under whatever the caller and the mounted elements already supply.
|
|
40
|
+
*
|
|
41
|
+
* Explicit input always wins. The sheet is only ever asked for an email or an address when no
|
|
42
|
+
* element is mounted to collect them, so in practice it fills exactly the gaps — which is the whole
|
|
43
|
+
* point: without this the mint refused a buyer who had just typed their email into Apple Pay.
|
|
44
|
+
*/
|
|
45
|
+
export function mergeWalletContact(
|
|
46
|
+
explicit: Partial<{ email: string; name: string }> | undefined,
|
|
47
|
+
contact: WalletContact,
|
|
48
|
+
): Partial<{ email: string; name: string }> & { address?: WalletContact['address'] } {
|
|
49
|
+
const merged: Partial<{ email: string; name: string }> & { address?: WalletContact['address'] } = {
|
|
50
|
+
...explicit,
|
|
51
|
+
};
|
|
52
|
+
if (!merged.email && contact.email) merged.email = contact.email;
|
|
53
|
+
if (!merged.name && contact.name) merged.name = contact.name;
|
|
54
|
+
if (contact.address && !merged.address) merged.address = contact.address;
|
|
55
|
+
return merged;
|
|
56
|
+
}
|
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
import { type ReactNode, useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';
|
|
2
|
+
import { type StyleProp, View, type ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
type CountryFormat,
|
|
6
|
+
type EmittedAddress,
|
|
7
|
+
loadCountryCatalog,
|
|
8
|
+
normalizeCountry,
|
|
9
|
+
parseCountryFormat,
|
|
10
|
+
type RemoteCountryData,
|
|
11
|
+
type StateOption,
|
|
12
|
+
} from '../core/address/engine';
|
|
13
|
+
import {
|
|
14
|
+
type AddressFormAction,
|
|
15
|
+
type AddressFormConfig,
|
|
16
|
+
addressFormReducer,
|
|
17
|
+
type AddressSnapshot,
|
|
18
|
+
type AddressValueKey,
|
|
19
|
+
allowedCountryList,
|
|
20
|
+
type CustomFieldSpec,
|
|
21
|
+
pairedGridRows,
|
|
22
|
+
planFields,
|
|
23
|
+
requiredFloorFor,
|
|
24
|
+
seedAddressForm,
|
|
25
|
+
snapshotAddressForm,
|
|
26
|
+
placeAllowList,
|
|
27
|
+
} from '../core/address/form-state';
|
|
28
|
+
import { countryName } from '../core/address/country-name';
|
|
29
|
+
import type { AddressSuggestion } from '../core/address/suggest';
|
|
30
|
+
import { resolveSubdivision } from '../core/address/suggest';
|
|
31
|
+
import { useAddressSuggestions } from '../core/address/use-suggestions';
|
|
32
|
+
import { WhopAddressSuggestions } from '../native';
|
|
33
|
+
import { SuggestionList } from '../ui/suggestions';
|
|
34
|
+
import { detectIpCountry } from '../core/address/places';
|
|
35
|
+
import { useLocale, useT } from '../i18n';
|
|
36
|
+
import { usePaymentsState, usePaymentsStore } from '../namespace';
|
|
37
|
+
import { useTheme } from '../theme/provider';
|
|
38
|
+
import { ElementFrame } from '../ui/element-frame';
|
|
39
|
+
import { Field, FieldSkeleton, Row, Stack, TextButton } from '../ui/primitives';
|
|
40
|
+
import { Select } from '../ui/select';
|
|
41
|
+
|
|
42
|
+
export interface AddressChangePayload {
|
|
43
|
+
complete: boolean;
|
|
44
|
+
address: EmittedAddress;
|
|
45
|
+
custom: Record<string, string>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface AddressElementProps {
|
|
49
|
+
/** Autocomplete purpose: `billing` (default) or `shipping`. */
|
|
50
|
+
mode?: 'billing' | 'shipping';
|
|
51
|
+
/** `full` stacks labeled fields. `compact` groups placeholder-labeled fields. */
|
|
52
|
+
layout?: 'compact' | 'full';
|
|
53
|
+
/** `full` follows the country's complete format. `minimal` collects country and postal code. */
|
|
54
|
+
scope?: 'minimal' | 'full';
|
|
55
|
+
defaultValues?: { name?: string; address?: Partial<EmittedAddress> };
|
|
56
|
+
/** Default the country from the buyer's IP. Falls back to `countryHint`, then US. */
|
|
57
|
+
detectCountry?: boolean;
|
|
58
|
+
allowedCountries?: string[];
|
|
59
|
+
name?: 'combined' | 'split' | 'none';
|
|
60
|
+
organization?: 'none' | 'name' | 'name_with_type';
|
|
61
|
+
line2?: 'toggle' | 'always' | 'never';
|
|
62
|
+
/**
|
|
63
|
+
* Street suggestions from the platform geocoder — MapKit on iOS, `android.location.Geocoder` on
|
|
64
|
+
* Android. No API key and no account: where the web element asks Google Places, this asks the OS.
|
|
65
|
+
* Falls back silently to manual entry on a device whose geocoder answers nothing.
|
|
66
|
+
*
|
|
67
|
+
* @default true — the same default the web element takes.
|
|
68
|
+
*/
|
|
69
|
+
autocomplete?: boolean;
|
|
70
|
+
customFields?: CustomFieldSpec[];
|
|
71
|
+
/** Fallback country after `defaultValues` and IP detection. ISO 3166-1 alpha-2. */
|
|
72
|
+
countryHint?: string;
|
|
73
|
+
style?: StyleProp<ViewStyle>;
|
|
74
|
+
fallback?: ReactNode;
|
|
75
|
+
onChange?: (payload: AddressChangePayload) => void;
|
|
76
|
+
onReady?: () => void;
|
|
77
|
+
onError?: (error: { message: string; code?: string }) => void;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Collects a postal address using the per-country format engine ported from the web element, so
|
|
82
|
+
* field order, labels, required-ness, postal validation, and the state lists are identical across
|
|
83
|
+
* the two surfaces for all 241 countries in the catalog.
|
|
84
|
+
*/
|
|
85
|
+
export function AddressElement({
|
|
86
|
+
mode = 'billing',
|
|
87
|
+
layout = 'full',
|
|
88
|
+
scope = 'full',
|
|
89
|
+
defaultValues,
|
|
90
|
+
detectCountry = true,
|
|
91
|
+
allowedCountries,
|
|
92
|
+
name = 'combined',
|
|
93
|
+
organization = 'none',
|
|
94
|
+
line2 = 'toggle',
|
|
95
|
+
autocomplete = true,
|
|
96
|
+
customFields,
|
|
97
|
+
countryHint = '',
|
|
98
|
+
style,
|
|
99
|
+
fallback,
|
|
100
|
+
onChange,
|
|
101
|
+
onReady,
|
|
102
|
+
onError,
|
|
103
|
+
}: AddressElementProps) {
|
|
104
|
+
const t = useT();
|
|
105
|
+
const theme = useTheme();
|
|
106
|
+
const store = usePaymentsStore();
|
|
107
|
+
const methodCountries = usePaymentsState((s) => s.methodCountries);
|
|
108
|
+
|
|
109
|
+
const [catalog, setCatalog] = useState<Record<string, RemoteCountryData> | null>(null);
|
|
110
|
+
const [detected, setDetected] = useState<string | undefined>(undefined);
|
|
111
|
+
const [detecting, setDetecting] = useState(detectCountry);
|
|
112
|
+
const [failure, setFailure] = useState<{ message: string; code?: string } | null>(null);
|
|
113
|
+
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
let alive = true;
|
|
116
|
+
loadCountryCatalog()
|
|
117
|
+
.then((data) => alive && setCatalog(data))
|
|
118
|
+
.catch((err: Error) => alive && setFailure({ message: err.message, code: 'ADDRESS_CATALOG_FAILED' }));
|
|
119
|
+
return () => {
|
|
120
|
+
alive = false;
|
|
121
|
+
};
|
|
122
|
+
}, []);
|
|
123
|
+
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (!detectCountry) return;
|
|
126
|
+
let alive = true;
|
|
127
|
+
detectIpCountry()
|
|
128
|
+
.then((country) => {
|
|
129
|
+
if (!alive) return;
|
|
130
|
+
setDetected(country);
|
|
131
|
+
setDetecting(false);
|
|
132
|
+
})
|
|
133
|
+
.catch(() => alive && setDetecting(false));
|
|
134
|
+
return () => {
|
|
135
|
+
alive = false;
|
|
136
|
+
};
|
|
137
|
+
}, [detectCountry]);
|
|
138
|
+
|
|
139
|
+
// The method constraint NARROWS the consumer's allow list, never replaces it — replacing would
|
|
140
|
+
// let a tier's countries widen a restriction the integrator set deliberately.
|
|
141
|
+
const effectiveAllowed = useMemo(
|
|
142
|
+
() => effectiveAllowedCountries(methodCountries ?? undefined, allowedCountries),
|
|
143
|
+
[methodCountries, allowedCountries],
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const config = useMemo<AddressFormConfig>(
|
|
147
|
+
() => ({
|
|
148
|
+
mode,
|
|
149
|
+
layout,
|
|
150
|
+
scope,
|
|
151
|
+
name,
|
|
152
|
+
organization,
|
|
153
|
+
line2,
|
|
154
|
+
autocomplete,
|
|
155
|
+
requiredFloor: requiredFloorFor(scope),
|
|
156
|
+
allowedCountries: effectiveAllowed,
|
|
157
|
+
customFields,
|
|
158
|
+
}),
|
|
159
|
+
[mode, layout, scope, name, organization, line2, autocomplete, effectiveAllowed, customFields],
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const ready = Boolean(catalog) && !detecting;
|
|
163
|
+
|
|
164
|
+
if (!catalog || detecting) {
|
|
165
|
+
return (
|
|
166
|
+
<ElementFrame
|
|
167
|
+
style={style}
|
|
168
|
+
fallback={fallback}
|
|
169
|
+
skeleton={
|
|
170
|
+
<Stack>
|
|
171
|
+
<FieldSkeleton />
|
|
172
|
+
<FieldSkeleton />
|
|
173
|
+
<FieldSkeleton />
|
|
174
|
+
</Stack>
|
|
175
|
+
}
|
|
176
|
+
ready={false}
|
|
177
|
+
error={failure}
|
|
178
|
+
onReady={onReady}
|
|
179
|
+
onError={onError}
|
|
180
|
+
>
|
|
181
|
+
<View />
|
|
182
|
+
</ElementFrame>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<AddressForm
|
|
188
|
+
catalog={catalog}
|
|
189
|
+
config={config}
|
|
190
|
+
detected={detected}
|
|
191
|
+
countryHint={countryHint}
|
|
192
|
+
defaultValues={defaultValues}
|
|
193
|
+
ready={ready}
|
|
194
|
+
style={style}
|
|
195
|
+
fallback={fallback}
|
|
196
|
+
onChange={onChange}
|
|
197
|
+
onReady={onReady}
|
|
198
|
+
onError={onError}
|
|
199
|
+
store={store}
|
|
200
|
+
t={t}
|
|
201
|
+
spacing={theme.tokens.spacing}
|
|
202
|
+
/>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Intersection when both exist; a disjoint pair keeps the consumer's list rather than widening. */
|
|
207
|
+
export function effectiveAllowedCountries(
|
|
208
|
+
methodCountries: string[] | undefined,
|
|
209
|
+
allowedCountries: string[] | undefined,
|
|
210
|
+
): string[] | undefined {
|
|
211
|
+
if (!methodCountries?.length) return allowedCountries;
|
|
212
|
+
if (!allowedCountries?.length) return methodCountries;
|
|
213
|
+
const consumerAllowed = new Set(allowedCountries.map((code) => code.toLowerCase()));
|
|
214
|
+
const intersection = methodCountries.filter((code) => consumerAllowed.has(code.toLowerCase()));
|
|
215
|
+
return intersection.length > 0 ? intersection : allowedCountries;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
interface AddressFormProps {
|
|
219
|
+
catalog: Record<string, RemoteCountryData>;
|
|
220
|
+
config: AddressFormConfig;
|
|
221
|
+
detected: string | undefined;
|
|
222
|
+
countryHint: string;
|
|
223
|
+
defaultValues: AddressElementProps['defaultValues'];
|
|
224
|
+
ready: boolean;
|
|
225
|
+
style?: StyleProp<ViewStyle>;
|
|
226
|
+
fallback?: ReactNode;
|
|
227
|
+
onChange?: (payload: AddressChangePayload) => void;
|
|
228
|
+
onReady?: () => void;
|
|
229
|
+
onError?: (error: { message: string; code?: string }) => void;
|
|
230
|
+
store: ReturnType<typeof usePaymentsStore>;
|
|
231
|
+
t: ReturnType<typeof useT>;
|
|
232
|
+
spacing: number;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function AddressForm({
|
|
236
|
+
catalog,
|
|
237
|
+
config,
|
|
238
|
+
detected,
|
|
239
|
+
countryHint,
|
|
240
|
+
defaultValues,
|
|
241
|
+
ready,
|
|
242
|
+
style,
|
|
243
|
+
fallback,
|
|
244
|
+
onChange,
|
|
245
|
+
onReady,
|
|
246
|
+
onError,
|
|
247
|
+
store,
|
|
248
|
+
t,
|
|
249
|
+
spacing,
|
|
250
|
+
}: AddressFormProps) {
|
|
251
|
+
const catalogCountries = useMemo(() => Object.keys(catalog), [catalog]);
|
|
252
|
+
|
|
253
|
+
const [state, rawDispatch] = useReducer(
|
|
254
|
+
addressFormReducer,
|
|
255
|
+
undefined,
|
|
256
|
+
() => seedAddressForm(config, { defaultValues, detectedCountry: detected, countryHint }, catalogCountries),
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
const format: CountryFormat = useMemo(
|
|
260
|
+
() => parseCountryFormat(state.country, catalog[state.country]),
|
|
261
|
+
[state.country, catalog],
|
|
262
|
+
);
|
|
263
|
+
const plan = useMemo(() => planFields(config, format), [config, format]);
|
|
264
|
+
const snapshot: AddressSnapshot = useMemo(
|
|
265
|
+
() => snapshotAddressForm(config, format, state),
|
|
266
|
+
[config, format, state],
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
const locale = useLocale();
|
|
270
|
+
const countryOptions = useMemo(() => {
|
|
271
|
+
const allowed = allowedCountryList(config, catalogCountries);
|
|
272
|
+
return allowed
|
|
273
|
+
.map((code) => ({ value: code, label: countryName(code, locale) }))
|
|
274
|
+
.sort((a, b) => a.label.localeCompare(b.label));
|
|
275
|
+
}, [config, catalogCountries, locale]);
|
|
276
|
+
|
|
277
|
+
const latestChange = useRef(onChange);
|
|
278
|
+
latestChange.current = onChange;
|
|
279
|
+
|
|
280
|
+
// The address element is the billing source while it is mounted, so the controller reads its
|
|
281
|
+
// snapshot at confirm rather than the payment element's internal block.
|
|
282
|
+
useEffect(() => {
|
|
283
|
+
store.patch({ addressLive: true });
|
|
284
|
+
return () => store.patch({ addressLive: false, addressCollected: undefined });
|
|
285
|
+
}, [store]);
|
|
286
|
+
|
|
287
|
+
useEffect(() => {
|
|
288
|
+
store.patch({
|
|
289
|
+
addressCollected: {
|
|
290
|
+
complete: snapshot.complete,
|
|
291
|
+
address: snapshot.address,
|
|
292
|
+
custom: snapshot.custom,
|
|
293
|
+
} as never,
|
|
294
|
+
});
|
|
295
|
+
latestChange.current?.({ complete: snapshot.complete, address: snapshot.address, custom: snapshot.custom });
|
|
296
|
+
}, [snapshot, store]);
|
|
297
|
+
|
|
298
|
+
const [line1Focused, setLine1Focused] = useState(false);
|
|
299
|
+
const suggestions = useAddressSuggestions(config.autocomplete, state.values.line1 ?? '');
|
|
300
|
+
const placeAllowed = useMemo(() => placeAllowList(config, catalogCountries), [config, catalogCountries]);
|
|
301
|
+
const showSuggestions = config.autocomplete && line1Focused && suggestions.length > 0;
|
|
302
|
+
|
|
303
|
+
// `apply-place` WHOLE-REPLACES the locality grid, so a resolve landing after the buyer moved on
|
|
304
|
+
// would silently rewrite line1, city, state and postal under them.
|
|
305
|
+
//
|
|
306
|
+
// The supersede is tracked with a counter bumped INSIDE dispatch rather than by reading the
|
|
307
|
+
// reducer's own `selecting` flag through a ref. A ref is only assigned during render, and an
|
|
308
|
+
// Android resolve is a cache hit that can settle before that render happens — which raced both
|
|
309
|
+
// ways: a fresh pick read a stale `false` and no-opped, and a superseding edit had not yet been
|
|
310
|
+
// seen, so a stale place still applied.
|
|
311
|
+
const pickSequence = useRef(0);
|
|
312
|
+
const dispatch = useCallback((action: AddressFormAction) => {
|
|
313
|
+
if (action.type === 'edit' || action.type === 'set-country') pickSequence.current += 1;
|
|
314
|
+
rawDispatch(action);
|
|
315
|
+
}, []);
|
|
316
|
+
|
|
317
|
+
const pickSuggestion = (suggestion: AddressSuggestion) => {
|
|
318
|
+
setLine1Focused(false);
|
|
319
|
+
const sequence = pickSequence.current + 1;
|
|
320
|
+
pickSequence.current = sequence;
|
|
321
|
+
dispatch({ type: 'begin-select' });
|
|
322
|
+
void WhopAddressSuggestions.resolve(suggestion.id, state.country).then((parsed) => {
|
|
323
|
+
if (pickSequence.current !== sequence) return;
|
|
324
|
+
if (!parsed) {
|
|
325
|
+
dispatch({ type: 'select-failed' });
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
// The state Select is keyed on the catalog's sub_key, and a geocoder may answer with the
|
|
329
|
+
// display name instead, so an unresolved subdivision must not reach the form as-is.
|
|
330
|
+
dispatch({
|
|
331
|
+
type: 'apply-place',
|
|
332
|
+
parsed: { ...parsed, state: resolveSubdivision(parsed.state, format.stateOptions) },
|
|
333
|
+
allowed: placeAllowed,
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const showError = (key: string) => (state.showAllErrors || state.touched[key] ? errorText(t, snapshot.errors[key]) : undefined);
|
|
339
|
+
const compact = config.layout === 'compact';
|
|
340
|
+
|
|
341
|
+
const textField = (key: AddressValueKey, label: string, extra?: Partial<Parameters<typeof Field>[0]>) => (
|
|
342
|
+
<Field
|
|
343
|
+
key={key}
|
|
344
|
+
label={compact ? undefined : label}
|
|
345
|
+
placeholder={compact ? label : undefined}
|
|
346
|
+
accessibilityLabel={label}
|
|
347
|
+
value={state.values[key] ?? ''}
|
|
348
|
+
onChangeText={(value) => dispatch({ type: 'edit', field: key, value })}
|
|
349
|
+
onBlur={() => dispatch({ type: 'blur', field: key })}
|
|
350
|
+
error={showError(key)}
|
|
351
|
+
containerStyle={{ flex: 1 }}
|
|
352
|
+
{...extra}
|
|
353
|
+
/>
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
const nameFields =
|
|
357
|
+
config.name === 'none'
|
|
358
|
+
? null
|
|
359
|
+
: config.name === 'split'
|
|
360
|
+
? (
|
|
361
|
+
<Row>
|
|
362
|
+
{textField('first_name', t('First name'), { autoComplete: 'given-name', textContentType: 'givenName' })}
|
|
363
|
+
{textField('last_name', t('Last name'), { autoComplete: 'family-name', textContentType: 'familyName' })}
|
|
364
|
+
</Row>
|
|
365
|
+
)
|
|
366
|
+
: textField('name', t('Name'), { autoComplete: 'name', textContentType: 'name' });
|
|
367
|
+
|
|
368
|
+
const organizationFields =
|
|
369
|
+
config.organization === 'none' ? null : (
|
|
370
|
+
<Stack gap={spacing * 2}>
|
|
371
|
+
{textField('organization', t('Organization'), { autoComplete: 'organization' })}
|
|
372
|
+
{config.organization === 'name_with_type' ? (
|
|
373
|
+
<Select
|
|
374
|
+
label={t('Organization type')}
|
|
375
|
+
value={state.organizationType || undefined}
|
|
376
|
+
options={[
|
|
377
|
+
{ value: 'business', label: t('Business') },
|
|
378
|
+
{ value: 'individual', label: t('Individual') },
|
|
379
|
+
]}
|
|
380
|
+
onChange={(value) =>
|
|
381
|
+
dispatch({ type: 'set-organization-type', value: value as 'business' | 'individual' })
|
|
382
|
+
}
|
|
383
|
+
/>
|
|
384
|
+
) : null}
|
|
385
|
+
</Stack>
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
const customAt = (position: CustomFieldSpec['position']) =>
|
|
389
|
+
(config.customFields ?? [])
|
|
390
|
+
.filter((field) => field.position === position)
|
|
391
|
+
.map((field) => {
|
|
392
|
+
const key = `custom:${field.key}`;
|
|
393
|
+
const value = state.custom[field.key] ?? '';
|
|
394
|
+
const onChange = (next: string) => dispatch({ type: 'edit-custom', key: field.key, value: next });
|
|
395
|
+
if (field.type === 'select') {
|
|
396
|
+
return (
|
|
397
|
+
<Select
|
|
398
|
+
key={key}
|
|
399
|
+
label={compact ? undefined : field.label}
|
|
400
|
+
placeholder={compact ? field.label : undefined}
|
|
401
|
+
accessibilityLabel={field.label}
|
|
402
|
+
value={value || undefined}
|
|
403
|
+
options={(field.options ?? []).map((option) => ({ value: option, label: option }))}
|
|
404
|
+
onChange={onChange}
|
|
405
|
+
error={showError(key)}
|
|
406
|
+
searchable={(field.options ?? []).length > 12}
|
|
407
|
+
/>
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
return (
|
|
411
|
+
<Field
|
|
412
|
+
key={key}
|
|
413
|
+
label={compact ? undefined : field.label}
|
|
414
|
+
placeholder={compact ? field.label : undefined}
|
|
415
|
+
accessibilityLabel={field.label}
|
|
416
|
+
value={value}
|
|
417
|
+
onChangeText={onChange}
|
|
418
|
+
onBlur={() => dispatch({ type: 'blur', field: key })}
|
|
419
|
+
error={showError(key)}
|
|
420
|
+
keyboardType={field.type === 'phone' ? 'phone-pad' : 'default'}
|
|
421
|
+
autoComplete={field.type === 'phone' ? 'tel' : undefined}
|
|
422
|
+
/>
|
|
423
|
+
);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
const gridRows = pairedGridRows(plan.gridFields);
|
|
427
|
+
|
|
428
|
+
return (
|
|
429
|
+
<ElementFrame
|
|
430
|
+
style={style}
|
|
431
|
+
fallback={fallback}
|
|
432
|
+
skeleton={
|
|
433
|
+
<Stack>
|
|
434
|
+
<FieldSkeleton />
|
|
435
|
+
<FieldSkeleton />
|
|
436
|
+
<FieldSkeleton />
|
|
437
|
+
</Stack>
|
|
438
|
+
}
|
|
439
|
+
ready={ready}
|
|
440
|
+
onReady={onReady}
|
|
441
|
+
onError={onError}
|
|
442
|
+
>
|
|
443
|
+
<Stack>
|
|
444
|
+
{nameFields}
|
|
445
|
+
{customAt('after_name')}
|
|
446
|
+
{organizationFields}
|
|
447
|
+
{customAt('after_organization')}
|
|
448
|
+
{customAt('before_country')}
|
|
449
|
+
<Select
|
|
450
|
+
label={t('Country')}
|
|
451
|
+
value={state.country}
|
|
452
|
+
options={countryOptions}
|
|
453
|
+
onChange={(country) => dispatch({ type: 'set-country', country: normalizeCountry(country) ?? country })}
|
|
454
|
+
searchable
|
|
455
|
+
partName="whop-AddressCountrySelect"
|
|
456
|
+
/>
|
|
457
|
+
{plan.gridFields.includes('line1') ? (
|
|
458
|
+
<View>
|
|
459
|
+
{textField('line1', gridLabel(t, 'line1', format), {
|
|
460
|
+
...gridExtras('line1', format),
|
|
461
|
+
onFocus: () => setLine1Focused(true),
|
|
462
|
+
onBlur: () => {
|
|
463
|
+
setLine1Focused(false);
|
|
464
|
+
dispatch({ type: 'blur', field: 'line1' });
|
|
465
|
+
},
|
|
466
|
+
})}
|
|
467
|
+
{showSuggestions ? <SuggestionList suggestions={suggestions} onPick={pickSuggestion} /> : null}
|
|
468
|
+
</View>
|
|
469
|
+
) : null}
|
|
470
|
+
{config.line2 === 'always' || state.line2Open ? textField('line2', t('Address line 2')) : null}
|
|
471
|
+
{gridRows.map((row) => (
|
|
472
|
+
<Row key={row.join('-')}>
|
|
473
|
+
{row.map((key) =>
|
|
474
|
+
key === 'state' && format.stateOptions.length > 0
|
|
475
|
+
? (
|
|
476
|
+
<View key={key} style={{ flex: 1 }}>
|
|
477
|
+
<Select
|
|
478
|
+
label={compact ? undefined : stateLabel(t, format)}
|
|
479
|
+
placeholder={compact ? stateLabel(t, format) : undefined}
|
|
480
|
+
accessibilityLabel={stateLabel(t, format)}
|
|
481
|
+
value={state.values.state || undefined}
|
|
482
|
+
options={format.stateOptions.map(toOption)}
|
|
483
|
+
onChange={(value) => dispatch({ type: 'edit', field: 'state', value })}
|
|
484
|
+
error={showError('state')}
|
|
485
|
+
searchable={format.stateOptions.length > 12}
|
|
486
|
+
/>
|
|
487
|
+
</View>
|
|
488
|
+
)
|
|
489
|
+
: textField(key, gridLabel(t, key, format), gridExtras(key, format)),
|
|
490
|
+
)}
|
|
491
|
+
</Row>
|
|
492
|
+
))}
|
|
493
|
+
{customAt('after_address')}
|
|
494
|
+
{config.line2 === 'toggle' && !state.line2Open ? (
|
|
495
|
+
<TextButton label={t('Add address line 2')} onPress={() => dispatch({ type: 'toggle-line2' })} />
|
|
496
|
+
) : null}
|
|
497
|
+
</Stack>
|
|
498
|
+
</ElementFrame>
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function toOption(option: StateOption): { value: string; label: string } {
|
|
503
|
+
// The engine keys options by the catalog's own sub_key; `iso` is display metadata, not the value
|
|
504
|
+
// the format engine validates against.
|
|
505
|
+
return { value: option.key, label: option.name };
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function stateLabel(t: ReturnType<typeof useT>, format: CountryFormat): string {
|
|
509
|
+
switch (format.stateLabel) {
|
|
510
|
+
case 'province':
|
|
511
|
+
return t('Province');
|
|
512
|
+
case 'emirate':
|
|
513
|
+
return t('Emirate');
|
|
514
|
+
case 'county':
|
|
515
|
+
return t('County');
|
|
516
|
+
case 'prefecture':
|
|
517
|
+
return t('Prefecture');
|
|
518
|
+
case 'island':
|
|
519
|
+
return t('Island');
|
|
520
|
+
case 'parish':
|
|
521
|
+
return t('Parish');
|
|
522
|
+
case 'department':
|
|
523
|
+
return t('Department');
|
|
524
|
+
case 'do_si':
|
|
525
|
+
return t('Province');
|
|
526
|
+
case 'oblast':
|
|
527
|
+
return t('Oblast');
|
|
528
|
+
case 'area':
|
|
529
|
+
return t('Area');
|
|
530
|
+
default:
|
|
531
|
+
return t('State');
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function gridLabel(t: ReturnType<typeof useT>, key: string, format: CountryFormat): string {
|
|
536
|
+
switch (key) {
|
|
537
|
+
case 'line1':
|
|
538
|
+
return t('Address line 1');
|
|
539
|
+
case 'city':
|
|
540
|
+
return t('City');
|
|
541
|
+
case 'state':
|
|
542
|
+
return stateLabel(t, format);
|
|
543
|
+
case 'postal_code':
|
|
544
|
+
return format.postalLabel === 'zip' ? t('ZIP code') : format.postalLabel === 'pin' ? t('PIN code') : t('Postal code');
|
|
545
|
+
default:
|
|
546
|
+
return key;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function gridExtras(key: string, format: CountryFormat): Partial<Parameters<typeof Field>[0]> {
|
|
551
|
+
if (key === 'postal_code') {
|
|
552
|
+
const field = format.fields.find((f) => f.type === 'postal_code');
|
|
553
|
+
return {
|
|
554
|
+
autoComplete: 'postal-code',
|
|
555
|
+
textContentType: 'postalCode',
|
|
556
|
+
autoCapitalize: 'characters',
|
|
557
|
+
placeholder: field?.postalPlaceholder,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
if (key === 'line1') return { autoComplete: 'address-line1', textContentType: 'streetAddressLine1' };
|
|
561
|
+
if (key === 'city') return { autoComplete: 'postal-address-locality', textContentType: 'addressCity' };
|
|
562
|
+
return {};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function errorText(t: ReturnType<typeof useT>, code: string | undefined): string | undefined {
|
|
566
|
+
if (code === 'required') return t('Required');
|
|
567
|
+
if (code === 'invalid') return t("That doesn't look right");
|
|
568
|
+
return undefined;
|
|
569
|
+
}
|
|
570
|
+
|