@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,87 @@
|
|
|
1
|
+
import { type ReactNode, useEffect } from 'react';
|
|
2
|
+
import { Pressable, type StyleProp, StyleSheet, Text, View, type ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { useT } from '../i18n';
|
|
5
|
+
import { usePaymentsStore } from '../namespace';
|
|
6
|
+
import { WhopAuthSession } from '../native';
|
|
7
|
+
import { alphaStep, useTheme } from '../theme/provider';
|
|
8
|
+
import { ElementFrame } from '../ui/element-frame';
|
|
9
|
+
import { part, Skeleton } from '../ui/primitives';
|
|
10
|
+
import { WhopLogo } from '../ui/whop-logo';
|
|
11
|
+
|
|
12
|
+
/** The embedded attribution the web element uses; an app mount is an embedded one. */
|
|
13
|
+
const WHOP_URL = 'https://whop.com/?a=tristan';
|
|
14
|
+
const TERMS_URL = 'https://whop.com/buyer-terms/';
|
|
15
|
+
const PRIVACY_URL = 'https://whop.com/privacy/';
|
|
16
|
+
|
|
17
|
+
export interface BrandingElementProps {
|
|
18
|
+
style?: StyleProp<ViewStyle>;
|
|
19
|
+
fallback?: ReactNode;
|
|
20
|
+
onReady?: () => void;
|
|
21
|
+
onError?: (error: { message: string; code?: string }) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Whop is the merchant of record on these sales, so the buyer is told so on the screen where they
|
|
26
|
+
* pay. Mounting this is what unlocks `createConfirmationToken` — a payment surface without the
|
|
27
|
+
* notice refuses rather than charging quietly.
|
|
28
|
+
*/
|
|
29
|
+
export function BrandingElement({ style, fallback, onReady, onError }: BrandingElementProps) {
|
|
30
|
+
const t = useT();
|
|
31
|
+
const theme = useTheme();
|
|
32
|
+
const store = usePaymentsStore();
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
store.patch({ brandingMounted: true });
|
|
36
|
+
return () => store.patch({ brandingMounted: false });
|
|
37
|
+
}, [store]);
|
|
38
|
+
|
|
39
|
+
const open = (url: string) => {
|
|
40
|
+
WhopAuthSession.present(url).catch(() => undefined);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const linkStyle = {
|
|
44
|
+
fontSize: 13,
|
|
45
|
+
lineHeight: 16,
|
|
46
|
+
fontFamily: theme.tokens.fontFamily,
|
|
47
|
+
color: alphaStep(theme.gray, 10),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<ElementFrame
|
|
52
|
+
style={style}
|
|
53
|
+
fallback={fallback}
|
|
54
|
+
skeleton={<Skeleton height={20} />}
|
|
55
|
+
ready
|
|
56
|
+
onReady={onReady}
|
|
57
|
+
onError={onError}
|
|
58
|
+
>
|
|
59
|
+
<View style={[styles.root, part(theme, 'whop-Branding')]}>
|
|
60
|
+
<Pressable accessibilityRole="link" onPress={() => open(WHOP_URL)} style={styles.brand} hitSlop={6}>
|
|
61
|
+
<Text style={[linkStyle, part(theme, 'whop-BrandingLink')]}>{t('Powered by')}</Text>
|
|
62
|
+
<WhopLogo height={12} />
|
|
63
|
+
</Pressable>
|
|
64
|
+
<View style={[styles.divider, { backgroundColor: alphaStep(theme.gray, 5) }]} />
|
|
65
|
+
<Pressable accessibilityRole="link" onPress={() => open(TERMS_URL)} hitSlop={6}>
|
|
66
|
+
<Text style={[linkStyle, part(theme, 'whop-BrandingLink')]}>{t('Terms')}</Text>
|
|
67
|
+
</Pressable>
|
|
68
|
+
<Pressable accessibilityRole="link" onPress={() => open(PRIVACY_URL)} hitSlop={6}>
|
|
69
|
+
<Text style={[linkStyle, part(theme, 'whop-BrandingLink')]}>{t('Privacy')}</Text>
|
|
70
|
+
</Pressable>
|
|
71
|
+
</View>
|
|
72
|
+
</ElementFrame>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const styles = StyleSheet.create({
|
|
77
|
+
root: {
|
|
78
|
+
flexDirection: 'row',
|
|
79
|
+
flexWrap: 'wrap',
|
|
80
|
+
alignItems: 'center',
|
|
81
|
+
justifyContent: 'center',
|
|
82
|
+
gap: 16,
|
|
83
|
+
paddingTop: 4,
|
|
84
|
+
},
|
|
85
|
+
brand: { flexDirection: 'row', alignItems: 'center', gap: 6 },
|
|
86
|
+
divider: { width: StyleSheet.hairlineWidth, height: 16 },
|
|
87
|
+
});
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
type MutableRefObject,
|
|
4
|
+
type ReactNode,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useRef,
|
|
10
|
+
useState,
|
|
11
|
+
} from 'react';
|
|
12
|
+
import { useBasisTheory } from '@basis-theory/react-native-elements';
|
|
13
|
+
|
|
14
|
+
import type { CardNetworkArt } from '../../api/types';
|
|
15
|
+
import {
|
|
16
|
+
type BasisTheoryElements,
|
|
17
|
+
type CardFieldFailure,
|
|
18
|
+
type CardFieldFailures,
|
|
19
|
+
tokenizeCard,
|
|
20
|
+
} from '../../core/payments/bt';
|
|
21
|
+
import { mergeBillingWithCollected } from '../../core/payments/billing-merge';
|
|
22
|
+
import { assertCardBilling, type CollectedPayment, type MintBillingDetails, validateBillingEmail } from '../../core/payments/mint';
|
|
23
|
+
import { PaymentTokenizationError } from '../../core/payments/errors';
|
|
24
|
+
import { PaymentsRefusalError } from '../../core/payments/refusals';
|
|
25
|
+
import { usePaymentsState, usePaymentsStore } from '../../namespace';
|
|
26
|
+
|
|
27
|
+
export interface CardFieldsChangePayload {
|
|
28
|
+
complete: boolean;
|
|
29
|
+
brand: string;
|
|
30
|
+
funding: string | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface CardFieldsState {
|
|
34
|
+
publicKey: string | undefined;
|
|
35
|
+
networks: CardNetworkArt[] | undefined;
|
|
36
|
+
layout: 'stacked' | 'compact';
|
|
37
|
+
bt: BasisTheoryElements | undefined;
|
|
38
|
+
loadError: string | null;
|
|
39
|
+
brand: string;
|
|
40
|
+
funding: string | null;
|
|
41
|
+
complete: { number: boolean; expiry: boolean; cvc: boolean };
|
|
42
|
+
errors: CardFieldFailures;
|
|
43
|
+
refs: MutableRefObject<{ number: unknown; expiryDate: unknown; cvc: unknown }>;
|
|
44
|
+
report: (field: 'number' | 'expiry' | 'cvc', event: FieldEvent) => void;
|
|
45
|
+
tokenize: () => Promise<string>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface FieldEvent {
|
|
49
|
+
complete: boolean;
|
|
50
|
+
brand?: string;
|
|
51
|
+
funding?: string | null;
|
|
52
|
+
empty?: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const CardFieldsContext = createContext<CardFieldsState | null | undefined>(undefined);
|
|
56
|
+
|
|
57
|
+
/** True while the fields are arranged by a composing unit (CardElement), false when a consumer
|
|
58
|
+
* mounted one directly. A standalone field draws its own label and full rounding. */
|
|
59
|
+
const CardComposedContext = createContext(false);
|
|
60
|
+
|
|
61
|
+
export function CardComposed({ children }: { children: ReactNode }) {
|
|
62
|
+
return <CardComposedContext.Provider value={true}>{children}</CardComposedContext.Provider>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function useCardComposed(): boolean {
|
|
66
|
+
return useContext(CardComposedContext);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function useCardFields(): CardFieldsState {
|
|
70
|
+
const value = useContext(CardFieldsContext);
|
|
71
|
+
if (value === undefined) {
|
|
72
|
+
throw new Error('Card field components must be used within <CardFields> or <CardElement>');
|
|
73
|
+
}
|
|
74
|
+
if (value === null) throw new Error('The card unit is not ready');
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface CardFieldsProviderProps {
|
|
79
|
+
/** Advanced Basis Theory publishable key. Omit it to fetch the key automatically. */
|
|
80
|
+
publicKey?: string;
|
|
81
|
+
/** The seller's accepted card networks in display order. Omit to fetch alongside the key. */
|
|
82
|
+
networks?: CardNetworkArt[];
|
|
83
|
+
/** `stacked` puts the number above expiry and cvc. `compact` puts all three in one row. */
|
|
84
|
+
layout?: 'stacked' | 'compact';
|
|
85
|
+
children: ReactNode;
|
|
86
|
+
onChange?: (payload: CardFieldsChangePayload) => void;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The card unit: one Basis Theory client, three field refs, and the tokenization that assembles
|
|
91
|
+
* them.
|
|
92
|
+
*
|
|
93
|
+
* The refs are opaque handles — an id and a formatter. The number the buyer typed lives inside the
|
|
94
|
+
* Basis Theory component and reaches the vault without passing through this object, which is the
|
|
95
|
+
* whole PCI story.
|
|
96
|
+
*/
|
|
97
|
+
export function CardFieldsProvider({
|
|
98
|
+
publicKey,
|
|
99
|
+
networks,
|
|
100
|
+
layout = 'stacked',
|
|
101
|
+
children,
|
|
102
|
+
onChange,
|
|
103
|
+
}: CardFieldsProviderProps) {
|
|
104
|
+
const store = usePaymentsStore();
|
|
105
|
+
const accountId = usePaymentsState((s) => s.accountId);
|
|
106
|
+
|
|
107
|
+
const [fetched, setFetched] = useState<{ publicKey?: string; networks?: CardNetworkArt[] } | null>(null);
|
|
108
|
+
const [configError, setConfigError] = useState<string | null>(null);
|
|
109
|
+
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (publicKey || !accountId) return;
|
|
112
|
+
let alive = true;
|
|
113
|
+
store.client.paymentMethodTypes
|
|
114
|
+
.retrieve('card', { account_id: accountId })
|
|
115
|
+
.then((entry) => {
|
|
116
|
+
if (!alive) return;
|
|
117
|
+
const key = entry.card?.public_key ?? undefined;
|
|
118
|
+
if (!key) {
|
|
119
|
+
setConfigError('card payments are not available for this account');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
setFetched({ publicKey: key, networks: entry.card?.networks });
|
|
123
|
+
})
|
|
124
|
+
.catch((err: Error) => alive && setConfigError(err.message));
|
|
125
|
+
return () => {
|
|
126
|
+
alive = false;
|
|
127
|
+
};
|
|
128
|
+
}, [publicKey, accountId, store]);
|
|
129
|
+
|
|
130
|
+
const resolvedKey = publicKey ?? fetched?.publicKey;
|
|
131
|
+
const resolvedNetworks = networks ?? fetched?.networks;
|
|
132
|
+
|
|
133
|
+
// Basis Theory is constructed ONLY once a real key exists. Initializing it with a placeholder
|
|
134
|
+
// makes the SDK surface its own "Please enter a valid API key" toast to the buyer, which is
|
|
135
|
+
// both wrong and unsuppressable — so the key gates the runtime instead. While it resolves the
|
|
136
|
+
// fields render their skeleton, which is what they would show anyway.
|
|
137
|
+
if (!resolvedKey) {
|
|
138
|
+
return (
|
|
139
|
+
<CardFieldsContext.Provider
|
|
140
|
+
value={{
|
|
141
|
+
publicKey: undefined,
|
|
142
|
+
networks: resolvedNetworks,
|
|
143
|
+
layout,
|
|
144
|
+
bt: undefined,
|
|
145
|
+
loadError: configError,
|
|
146
|
+
brand: 'unknown',
|
|
147
|
+
funding: null,
|
|
148
|
+
complete: { number: false, expiry: false, cvc: false },
|
|
149
|
+
errors: {},
|
|
150
|
+
refs: PENDING_REFS,
|
|
151
|
+
report: noop,
|
|
152
|
+
tokenize: notReady,
|
|
153
|
+
}}
|
|
154
|
+
>
|
|
155
|
+
{children}
|
|
156
|
+
</CardFieldsContext.Provider>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<CardFieldsRuntime
|
|
162
|
+
publicKey={resolvedKey}
|
|
163
|
+
networks={resolvedNetworks}
|
|
164
|
+
layout={layout}
|
|
165
|
+
configError={configError}
|
|
166
|
+
onChange={onChange}
|
|
167
|
+
>
|
|
168
|
+
{children}
|
|
169
|
+
</CardFieldsRuntime>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const PENDING_REFS = { current: { number: null, expiryDate: null, cvc: null } };
|
|
174
|
+
const noop = () => undefined;
|
|
175
|
+
const notReady = (): Promise<string> => {
|
|
176
|
+
throw new PaymentTokenizationError('card_fields_not_ready', 'The card tokenizer is not ready yet');
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
function CardFieldsRuntime({
|
|
180
|
+
publicKey,
|
|
181
|
+
networks,
|
|
182
|
+
layout,
|
|
183
|
+
configError,
|
|
184
|
+
children,
|
|
185
|
+
onChange,
|
|
186
|
+
}: {
|
|
187
|
+
publicKey: string;
|
|
188
|
+
networks: CardNetworkArt[] | undefined;
|
|
189
|
+
layout: 'stacked' | 'compact';
|
|
190
|
+
configError: string | null;
|
|
191
|
+
children: ReactNode;
|
|
192
|
+
onChange?: (payload: CardFieldsChangePayload) => void;
|
|
193
|
+
}) {
|
|
194
|
+
const store = usePaymentsStore();
|
|
195
|
+
const accountId = usePaymentsState((s) => s.accountId);
|
|
196
|
+
const { bt, error: btError } = useBasisTheory(publicKey);
|
|
197
|
+
|
|
198
|
+
const refs = useRef<{ number: unknown; expiryDate: unknown; cvc: unknown }>({
|
|
199
|
+
number: null,
|
|
200
|
+
expiryDate: null,
|
|
201
|
+
cvc: null,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const [brand, setBrand] = useState('unknown');
|
|
205
|
+
const [funding, setFunding] = useState<string | null>(null);
|
|
206
|
+
const [complete, setComplete] = useState({ number: false, expiry: false, cvc: false });
|
|
207
|
+
const [errors, setErrors] = useState<CardFieldFailures>({});
|
|
208
|
+
|
|
209
|
+
const latestChange = useRef(onChange);
|
|
210
|
+
latestChange.current = onChange;
|
|
211
|
+
|
|
212
|
+
const allComplete = complete.number && complete.expiry && complete.cvc;
|
|
213
|
+
useEffect(() => {
|
|
214
|
+
latestChange.current?.({ complete: allComplete, brand, funding });
|
|
215
|
+
}, [allComplete, brand, funding]);
|
|
216
|
+
|
|
217
|
+
const report = useCallback((field: 'number' | 'expiry' | 'cvc', event: FieldEvent) => {
|
|
218
|
+
setComplete((prev) => (prev[field] === event.complete ? prev : { ...prev, [field]: event.complete }));
|
|
219
|
+
if (event.brand !== undefined) setBrand(event.brand || 'unknown');
|
|
220
|
+
if (event.funding !== undefined) setFunding(event.funding ?? null);
|
|
221
|
+
// A field the buyer is fixing should not keep wearing the last failure.
|
|
222
|
+
setErrors((prev) => (prev[field] === undefined ? prev : { ...prev, [field]: undefined }));
|
|
223
|
+
}, []);
|
|
224
|
+
|
|
225
|
+
const tokenize = useCallback(async (): Promise<string> => {
|
|
226
|
+
if (!bt) {
|
|
227
|
+
throw new PaymentTokenizationError('card_fields_not_ready', 'The card tokenizer is not ready yet');
|
|
228
|
+
}
|
|
229
|
+
if (!accountId) {
|
|
230
|
+
throw new PaymentsRefusalError('CHARGE_CONFIG_MISSING', 'No seller account is in scope for this card unit');
|
|
231
|
+
}
|
|
232
|
+
const expiry = refs.current.expiryDate as { month?: () => unknown; year?: () => unknown } | null;
|
|
233
|
+
const result = await tokenizeCard(
|
|
234
|
+
bt,
|
|
235
|
+
{
|
|
236
|
+
number: refs.current.number,
|
|
237
|
+
expiryMonth: expiry?.month?.() ?? null,
|
|
238
|
+
expiryYear: expiry?.year?.() ?? null,
|
|
239
|
+
cvc: refs.current.cvc,
|
|
240
|
+
},
|
|
241
|
+
{ accountId, publicKey },
|
|
242
|
+
store.client,
|
|
243
|
+
);
|
|
244
|
+
if (!result.ok) {
|
|
245
|
+
if (result.fields) setErrors(result.fields);
|
|
246
|
+
throw new PaymentTokenizationError(result.code, result.message);
|
|
247
|
+
}
|
|
248
|
+
setErrors({});
|
|
249
|
+
return result.token;
|
|
250
|
+
}, [bt, accountId, store, publicKey]);
|
|
251
|
+
|
|
252
|
+
const value = useMemo<CardFieldsState>(
|
|
253
|
+
() => ({
|
|
254
|
+
publicKey,
|
|
255
|
+
networks,
|
|
256
|
+
layout,
|
|
257
|
+
bt,
|
|
258
|
+
loadError: configError ?? btError?.message ?? null,
|
|
259
|
+
brand,
|
|
260
|
+
funding,
|
|
261
|
+
complete,
|
|
262
|
+
errors,
|
|
263
|
+
refs,
|
|
264
|
+
report,
|
|
265
|
+
tokenize,
|
|
266
|
+
}),
|
|
267
|
+
[publicKey, networks, layout, bt, configError, btError, brand, funding, complete, errors, report, tokenize],
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
return <CardFieldsContext.Provider value={value}>{children}</CardFieldsContext.Provider>;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Registers the unit as the handle's collection surface. `card` and `cardFields` register under
|
|
275
|
+
* different kinds so the confirm cascade resolves the same order the web controller does.
|
|
276
|
+
*/
|
|
277
|
+
export function useCardCollection(kind: 'card' | 'cardFields'): void {
|
|
278
|
+
const store = usePaymentsStore();
|
|
279
|
+
const unit = useCardFields();
|
|
280
|
+
const latest = useRef(unit);
|
|
281
|
+
latest.current = unit;
|
|
282
|
+
|
|
283
|
+
useEffect(
|
|
284
|
+
() =>
|
|
285
|
+
store.registerSurface({
|
|
286
|
+
kind,
|
|
287
|
+
collect: async (input): Promise<CollectedPayment> => {
|
|
288
|
+
// A standalone CardElement is its own collection surface, with no payment element above
|
|
289
|
+
// it to fold the address in first — so it merges here for the same reason. The billing
|
|
290
|
+
// NAME comes from the address element and is otherwise merged only after collect
|
|
291
|
+
// returns, which rejected a name the form had already collected.
|
|
292
|
+
const seeded = validateBillingEmail(input?.billingDetails);
|
|
293
|
+
const settled = store.getSnapshot();
|
|
294
|
+
const billingDetails = settled.addressLive
|
|
295
|
+
? ((mergeBillingWithCollected(seeded, settled.addressCollected) as MintBillingDetails | undefined) ??
|
|
296
|
+
seeded)
|
|
297
|
+
: seeded;
|
|
298
|
+
assertCardBilling(billingDetails);
|
|
299
|
+
const token = await latest.current.tokenize();
|
|
300
|
+
return {
|
|
301
|
+
paymentMethod: { type: 'card', category: 'card', card: { token } },
|
|
302
|
+
billingDetails,
|
|
303
|
+
};
|
|
304
|
+
},
|
|
305
|
+
}),
|
|
306
|
+
[store, kind],
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export type { CardFieldFailure };
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { type ReactNode, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { type StyleProp, StyleSheet, Text, View, type ViewStyle } from 'react-native';
|
|
3
|
+
import {
|
|
4
|
+
CardExpirationDateElement,
|
|
5
|
+
CardNumberElement as BtCardNumberElement,
|
|
6
|
+
CardVerificationCodeElement,
|
|
7
|
+
type ElementEvent,
|
|
8
|
+
} from '@basis-theory/react-native-elements';
|
|
9
|
+
|
|
10
|
+
import type { PartName } from '../../theme/appearance';
|
|
11
|
+
import { useT } from '../../i18n';
|
|
12
|
+
import { alphaStep, step, useTheme } from '../../theme/provider';
|
|
13
|
+
import { ElementFrame } from '../../ui/element-frame';
|
|
14
|
+
import { FieldSkeleton, part, useFieldStyles } from '../../ui/primitives';
|
|
15
|
+
import { type CardFieldFailure, useCardComposed, useCardFields } from './context';
|
|
16
|
+
|
|
17
|
+
export interface CardFieldProps {
|
|
18
|
+
style?: StyleProp<ViewStyle>;
|
|
19
|
+
fallback?: ReactNode;
|
|
20
|
+
onReady?: () => void;
|
|
21
|
+
onError?: (error: { message: string; code?: string }) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type Slot = 'number' | 'expiry' | 'cvc';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Which corners a slot rounds depends on how the composing unit arranged it. A separately mounted
|
|
28
|
+
* field ignores the unit's layout and draws fully independent chrome, which is what the web
|
|
29
|
+
* element documents.
|
|
30
|
+
*/
|
|
31
|
+
function corners(slot: Slot, layout: 'stacked' | 'compact', standalone: boolean, radius: number): ViewStyle {
|
|
32
|
+
if (standalone) return { borderRadius: radius };
|
|
33
|
+
if (layout === 'compact') {
|
|
34
|
+
if (slot === 'number') return { borderTopLeftRadius: radius, borderBottomLeftRadius: radius };
|
|
35
|
+
if (slot === 'cvc') return { borderTopRightRadius: radius, borderBottomRightRadius: radius };
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
if (slot === 'number') return { borderTopLeftRadius: radius, borderTopRightRadius: radius };
|
|
39
|
+
if (slot === 'expiry') return { borderBottomLeftRadius: radius };
|
|
40
|
+
return { borderBottomRightRadius: radius };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface SlotShellProps {
|
|
44
|
+
slot: Slot;
|
|
45
|
+
label: string;
|
|
46
|
+
error: CardFieldFailure;
|
|
47
|
+
focused: boolean;
|
|
48
|
+
children: ReactNode;
|
|
49
|
+
trailing?: ReactNode;
|
|
50
|
+
partName: PartName;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function SlotShell({ slot, label, error, focused, children, trailing, partName }: SlotShellProps) {
|
|
54
|
+
const t = useT();
|
|
55
|
+
const s = useFieldStyles();
|
|
56
|
+
const theme = s.theme;
|
|
57
|
+
const unit = useCardFields();
|
|
58
|
+
const standalone = !useCardComposed();
|
|
59
|
+
const invalid = Boolean(error);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<View style={{ gap: theme.tokens.spacing }}>
|
|
63
|
+
{standalone ? <Text style={[s.label, part(theme, 'whop-Label')]}>{label}</Text> : null}
|
|
64
|
+
<View
|
|
65
|
+
style={[
|
|
66
|
+
s.box({ focused, invalid, disabled: false }),
|
|
67
|
+
corners(slot, unit.layout, standalone, theme.tokens.radius),
|
|
68
|
+
styles.row,
|
|
69
|
+
part(theme, 'whop-Input'),
|
|
70
|
+
focused ? part(theme, 'whop-InputFocused') : null,
|
|
71
|
+
invalid ? part(theme, 'whop-InputInvalid') : null,
|
|
72
|
+
part(theme, partName),
|
|
73
|
+
]}
|
|
74
|
+
>
|
|
75
|
+
<View style={styles.grow}>{children}</View>
|
|
76
|
+
{trailing}
|
|
77
|
+
</View>
|
|
78
|
+
{invalid ? (
|
|
79
|
+
<Text style={[s.error, part(theme, 'whop-ErrorText')]}>
|
|
80
|
+
{error === 'incomplete' ? t('Complete this field') : t("That doesn't look right")}
|
|
81
|
+
</Text>
|
|
82
|
+
) : null}
|
|
83
|
+
</View>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The Basis Theory element owns its own text; the chrome around it is ours. */
|
|
88
|
+
function useBtInputStyle() {
|
|
89
|
+
const s = useFieldStyles();
|
|
90
|
+
return {
|
|
91
|
+
color: s.input.color,
|
|
92
|
+
fontSize: s.input.fontSize,
|
|
93
|
+
fontFamily: s.input.fontFamily,
|
|
94
|
+
height: s.theme.tokens.controlHeight - 2,
|
|
95
|
+
padding: 0,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function useSlot(assign: (unit: ReturnType<typeof useCardFields>, ref: unknown) => void) {
|
|
100
|
+
const unit = useCardFields();
|
|
101
|
+
const [focused, setFocused] = useState(false);
|
|
102
|
+
const btRef = useRef<unknown>(null);
|
|
103
|
+
|
|
104
|
+
// The Basis Theory ref settles after the component's own first commit, so assigning on every
|
|
105
|
+
// render is what keeps the unit's handle live across a re-render.
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
assign(unit, btRef.current);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
unit,
|
|
112
|
+
btRef,
|
|
113
|
+
focused,
|
|
114
|
+
setFocused,
|
|
115
|
+
ready: Boolean(unit.bt) || Boolean(unit.loadError),
|
|
116
|
+
error: unit.loadError ? { message: unit.loadError, code: 'CARD_TOKENIZER_UNAVAILABLE' } : null,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function CardNumberElement({ style, fallback, onReady, onError }: CardFieldProps) {
|
|
121
|
+
const t = useT();
|
|
122
|
+
const theme = useTheme();
|
|
123
|
+
const inputStyle = useBtInputStyle();
|
|
124
|
+
const { unit, btRef, focused, setFocused, ready, error } = useSlot((u, ref) => {
|
|
125
|
+
u.refs.current.number = ref;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<ElementFrame
|
|
130
|
+
style={style}
|
|
131
|
+
fallback={fallback}
|
|
132
|
+
skeleton={<FieldSkeleton />}
|
|
133
|
+
ready={ready}
|
|
134
|
+
error={error}
|
|
135
|
+
onReady={onReady}
|
|
136
|
+
onError={onError}
|
|
137
|
+
>
|
|
138
|
+
<SlotShell
|
|
139
|
+
slot="number"
|
|
140
|
+
label={t('Card number')}
|
|
141
|
+
error={unit.errors.number}
|
|
142
|
+
focused={focused}
|
|
143
|
+
partName="whop-CardNumberField"
|
|
144
|
+
trailing={<BrandArt brand={unit.brand} />}
|
|
145
|
+
>
|
|
146
|
+
<BtCardNumberElement
|
|
147
|
+
btRef={btRef as never}
|
|
148
|
+
placeholder="1234 1234 1234 1234"
|
|
149
|
+
placeholderTextColor={alphaStep(theme.gray, 10)}
|
|
150
|
+
style={inputStyle as never}
|
|
151
|
+
onFocus={() => setFocused(true)}
|
|
152
|
+
onBlur={() => setFocused(false)}
|
|
153
|
+
onChange={(event: ElementEvent) =>
|
|
154
|
+
unit.report('number', {
|
|
155
|
+
complete: event.complete,
|
|
156
|
+
brand: event.brand,
|
|
157
|
+
funding: event.binInfo?.funding ?? null,
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
/>
|
|
161
|
+
</SlotShell>
|
|
162
|
+
</ElementFrame>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function CardExpiryElement({ style, fallback, onReady, onError }: CardFieldProps) {
|
|
167
|
+
const t = useT();
|
|
168
|
+
const theme = useTheme();
|
|
169
|
+
const inputStyle = useBtInputStyle();
|
|
170
|
+
const { unit, btRef, focused, setFocused, ready, error } = useSlot((u, ref) => {
|
|
171
|
+
u.refs.current.expiryDate = ref;
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<ElementFrame
|
|
176
|
+
style={style}
|
|
177
|
+
fallback={fallback}
|
|
178
|
+
skeleton={<FieldSkeleton />}
|
|
179
|
+
ready={ready}
|
|
180
|
+
error={error}
|
|
181
|
+
onReady={onReady}
|
|
182
|
+
onError={onError}
|
|
183
|
+
>
|
|
184
|
+
<SlotShell
|
|
185
|
+
slot="expiry"
|
|
186
|
+
label={t('Expiration')}
|
|
187
|
+
error={unit.errors.expiry}
|
|
188
|
+
focused={focused}
|
|
189
|
+
partName="whop-CardExpiryField"
|
|
190
|
+
>
|
|
191
|
+
<CardExpirationDateElement
|
|
192
|
+
btRef={btRef as never}
|
|
193
|
+
placeholder="MM / YY"
|
|
194
|
+
placeholderTextColor={alphaStep(theme.gray, 10)}
|
|
195
|
+
style={inputStyle as never}
|
|
196
|
+
onFocus={() => setFocused(true)}
|
|
197
|
+
onBlur={() => setFocused(false)}
|
|
198
|
+
onChange={(event: ElementEvent) => unit.report('expiry', { complete: event.complete })}
|
|
199
|
+
/>
|
|
200
|
+
</SlotShell>
|
|
201
|
+
</ElementFrame>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function CardCvcElement({ style, fallback, onReady, onError }: CardFieldProps) {
|
|
206
|
+
const t = useT();
|
|
207
|
+
const theme = useTheme();
|
|
208
|
+
const inputStyle = useBtInputStyle();
|
|
209
|
+
const { unit, btRef, focused, setFocused, ready, error } = useSlot((u, ref) => {
|
|
210
|
+
u.refs.current.cvc = ref;
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<ElementFrame
|
|
215
|
+
style={style}
|
|
216
|
+
fallback={fallback}
|
|
217
|
+
skeleton={<FieldSkeleton />}
|
|
218
|
+
ready={ready}
|
|
219
|
+
error={error}
|
|
220
|
+
onReady={onReady}
|
|
221
|
+
onError={onError}
|
|
222
|
+
>
|
|
223
|
+
<SlotShell
|
|
224
|
+
slot="cvc"
|
|
225
|
+
label={t('Security code')}
|
|
226
|
+
error={unit.errors.cvc}
|
|
227
|
+
focused={focused}
|
|
228
|
+
partName="whop-CardCvcField"
|
|
229
|
+
>
|
|
230
|
+
<CardVerificationCodeElement
|
|
231
|
+
btRef={btRef as never}
|
|
232
|
+
// amex prints a four-digit code; every other network prints three.
|
|
233
|
+
placeholder={unit.brand === 'american-express' ? '1234' : '123'}
|
|
234
|
+
placeholderTextColor={alphaStep(theme.gray, 10)}
|
|
235
|
+
style={inputStyle as never}
|
|
236
|
+
onFocus={() => setFocused(true)}
|
|
237
|
+
onBlur={() => setFocused(false)}
|
|
238
|
+
onChange={(event: ElementEvent) => unit.report('cvc', { complete: event.complete })}
|
|
239
|
+
/>
|
|
240
|
+
</SlotShell>
|
|
241
|
+
</ElementFrame>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function BrandArt({ brand }: { brand: string }) {
|
|
246
|
+
const theme = useTheme();
|
|
247
|
+
if (!brand || brand === 'unknown') return null;
|
|
248
|
+
const label = BRAND_LABELS[brand] ?? brand.slice(0, 4).toUpperCase();
|
|
249
|
+
return (
|
|
250
|
+
<View
|
|
251
|
+
style={{
|
|
252
|
+
paddingHorizontal: 6,
|
|
253
|
+
paddingVertical: 2,
|
|
254
|
+
borderRadius: 4,
|
|
255
|
+
backgroundColor: alphaStep(theme.gray, 3),
|
|
256
|
+
}}
|
|
257
|
+
>
|
|
258
|
+
<Text style={{ fontSize: 11, fontWeight: '600', color: step(theme.gray, 11) }}>{label}</Text>
|
|
259
|
+
</View>
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const BRAND_LABELS: Record<string, string> = {
|
|
264
|
+
visa: 'VISA',
|
|
265
|
+
mastercard: 'MC',
|
|
266
|
+
'american-express': 'AMEX',
|
|
267
|
+
discover: 'DISC',
|
|
268
|
+
'diners-club': 'DINERS',
|
|
269
|
+
jcb: 'JCB',
|
|
270
|
+
unionpay: 'UP',
|
|
271
|
+
maestro: 'MAESTRO',
|
|
272
|
+
elo: 'ELO',
|
|
273
|
+
mir: 'MIR',
|
|
274
|
+
hiper: 'HIPER',
|
|
275
|
+
hipercard: 'HIPER',
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const styles = StyleSheet.create({
|
|
279
|
+
row: { flexDirection: 'row', alignItems: 'center' },
|
|
280
|
+
grow: { flex: 1 },
|
|
281
|
+
});
|