@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,497 @@
|
|
|
1
|
+
import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { Platform, Pressable, type StyleProp, StyleSheet, Text, View, type ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import type { CardNetworkArt } from '../../api/types';
|
|
5
|
+
import type { CollectedPayment } from '../../core/payments/mint';
|
|
6
|
+
import {
|
|
7
|
+
detailTemplate,
|
|
8
|
+
formatMinorAmount,
|
|
9
|
+
type MatrixEntry,
|
|
10
|
+
methodBillingCountries,
|
|
11
|
+
orderRank,
|
|
12
|
+
presentMethod,
|
|
13
|
+
type WalletAvailability,
|
|
14
|
+
} from '../../core/payments/detail';
|
|
15
|
+
import { PaymentsRefusalError } from '../../core/payments/refusals';
|
|
16
|
+
import { mergeBillingWithCollected } from '../../core/payments/billing-merge';
|
|
17
|
+
import { assertCardBilling, type MintBillingDetails, validateBillingEmail } from '../../core/payments/mint';
|
|
18
|
+
import { useLocale, useT } from '../../i18n';
|
|
19
|
+
import { usePaymentsState, usePaymentsStore } from '../../namespace';
|
|
20
|
+
import { WALLET_TYPE_FOR_PLATFORM, WhopPlatformPay } from '../../native';
|
|
21
|
+
import { alphaStep, step, useTheme } from '../../theme/provider';
|
|
22
|
+
import { ElementFrame } from '../../ui/element-frame';
|
|
23
|
+
import { ErrorFace, part, Skeleton, Stack } from '../../ui/primitives';
|
|
24
|
+
import { PaymentMethodIcon } from '../../ui/payment-method-icon';
|
|
25
|
+
import { CardElement } from '../card';
|
|
26
|
+
import { DeclaredFields, useDeclaredFields } from './declared-fields';
|
|
27
|
+
import { mergeWalletContact } from '../../core/payments/wallet-contact';
|
|
28
|
+
import { collectWallet, useWalletAvailability, WalletPayButton, walletRequestFor } from './wallet';
|
|
29
|
+
|
|
30
|
+
export interface PaymentChangePayload {
|
|
31
|
+
complete: boolean;
|
|
32
|
+
type?: string;
|
|
33
|
+
supportsBuyerFee?: boolean;
|
|
34
|
+
method?: {
|
|
35
|
+
type: string;
|
|
36
|
+
category: string;
|
|
37
|
+
template: string;
|
|
38
|
+
display_name: string;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface PaymentElementProps {
|
|
43
|
+
/** Method display order, Stripe's `paymentMethodOrder` semantics. Unlisted types keep their
|
|
44
|
+
* incoming relative order behind the listed ones. */
|
|
45
|
+
order?: string[];
|
|
46
|
+
style?: StyleProp<ViewStyle>;
|
|
47
|
+
fallback?: ReactNode;
|
|
48
|
+
onChange?: (payload: PaymentChangePayload) => void;
|
|
49
|
+
onReady?: () => void;
|
|
50
|
+
onError?: (error: { message: string; code?: string }) => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Shows the payment methods offered for this charge and collects the selected one's required
|
|
55
|
+
* fields.
|
|
56
|
+
*
|
|
57
|
+
* The offered set is the matrix filtered by the same eight `presentMethod` gates the web element
|
|
58
|
+
* applies, from the same inputs. The surface is declared `direct_checkout` because an app owns its
|
|
59
|
+
* own presentation the way a first-party document does — and unlike an iframe it can hand a
|
|
60
|
+
* full-page step to the system browser, so `full_page`-only methods stay in the list.
|
|
61
|
+
*/
|
|
62
|
+
export function PaymentElement({ order, style, fallback, onChange, onReady, onError }: PaymentElementProps) {
|
|
63
|
+
const t = useT();
|
|
64
|
+
const locale = useLocale();
|
|
65
|
+
const theme = useTheme();
|
|
66
|
+
const store = usePaymentsStore();
|
|
67
|
+
const charge = usePaymentsState((s) => s.charge);
|
|
68
|
+
const accountId = usePaymentsState((s) => s.accountId);
|
|
69
|
+
const addressLive = usePaymentsState((s) => s.addressLive);
|
|
70
|
+
// Reactive, like addressLive beside it. Read through store.getSnapshot() inside the walletRequest
|
|
71
|
+
// memo this went stale: the store's identity never changes, so clearing an email that had been
|
|
72
|
+
// present left requiresEmail false and the sheet stopped collecting one.
|
|
73
|
+
const buyerEmail = usePaymentsState((s) => s.buyerEmail);
|
|
74
|
+
const options = store.getOptions();
|
|
75
|
+
|
|
76
|
+
const [entries, setEntries] = useState<MatrixEntry[] | null>(null);
|
|
77
|
+
const [failure, setFailure] = useState<{ message: string; code?: string } | null>(null);
|
|
78
|
+
const [selectedType, setSelectedType] = useState<string | undefined>(undefined);
|
|
79
|
+
|
|
80
|
+
const currency = charge.configured ? charge.currency : undefined;
|
|
81
|
+
const amount = charge.configured ? charge.amount : undefined;
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (!accountId || !currency || amount === undefined) return;
|
|
85
|
+
let alive = true;
|
|
86
|
+
setEntries(null);
|
|
87
|
+
store.client.paymentMethodTypes
|
|
88
|
+
.list({ account_id: accountId, currency, amount, surface: store.surface })
|
|
89
|
+
.then((page) => alive && setEntries(page.data as MatrixEntry[]))
|
|
90
|
+
.catch((err: Error) => alive && setFailure({ message: err.message, code: 'PAYMENT_METHODS_FAILED' }));
|
|
91
|
+
return () => {
|
|
92
|
+
alive = false;
|
|
93
|
+
};
|
|
94
|
+
}, [accountId, currency, amount, store]);
|
|
95
|
+
|
|
96
|
+
const walletEntry = useMemo(
|
|
97
|
+
() => entries?.find((entry) => entry.type === WALLET_TYPE_FOR_PLATFORM),
|
|
98
|
+
[entries],
|
|
99
|
+
);
|
|
100
|
+
const walletAvailable = useWalletAvailability(walletEntry);
|
|
101
|
+
|
|
102
|
+
const availability = useMemo<WalletAvailability | undefined>(() => {
|
|
103
|
+
if (walletAvailable === undefined) return undefined;
|
|
104
|
+
return Platform.OS === 'ios' ? { applePay: walletAvailable } : { googlePay: walletAvailable };
|
|
105
|
+
}, [walletAvailable]);
|
|
106
|
+
|
|
107
|
+
// Declared ahead of `offers`, which filters on it. A `const` read from a closure that runs during
|
|
108
|
+
// the same render is a temporal dead zone, not a hoist: this threw
|
|
109
|
+
// "Cannot access 'billingCountry' before initialization" and the element painted no tiles at all.
|
|
110
|
+
const billingCountry = usePaymentsState((s) => s.addressCollected?.address?.country);
|
|
111
|
+
|
|
112
|
+
const offers = useMemo(() => {
|
|
113
|
+
if (!entries || !charge.configured) return [];
|
|
114
|
+
const filtered = entries.filter((entry) =>
|
|
115
|
+
presentMethod(
|
|
116
|
+
entry,
|
|
117
|
+
charge.currency,
|
|
118
|
+
charge.amount,
|
|
119
|
+
charge.setupFutureUsage,
|
|
120
|
+
availability,
|
|
121
|
+
charge.paymentMethodConfiguration,
|
|
122
|
+
store.surface,
|
|
123
|
+
billingCountry,
|
|
124
|
+
),
|
|
125
|
+
);
|
|
126
|
+
const ranking = order ?? options.order ?? [];
|
|
127
|
+
return filtered
|
|
128
|
+
.map((entry, index) => ({ entry, index }))
|
|
129
|
+
.sort((a, b) => orderRank(a.entry.type, ranking) - orderRank(b.entry.type, ranking) || a.index - b.index)
|
|
130
|
+
.map(({ entry }) => entry);
|
|
131
|
+
}, [entries, charge, availability, order, options.order, store.surface, billingCountry]);
|
|
132
|
+
|
|
133
|
+
const selected = offers.find((entry) => entry.type === selectedType);
|
|
134
|
+
const template = selected ? detailTemplate(selected) : undefined;
|
|
135
|
+
|
|
136
|
+
const declared = useDeclaredFields(selected, billingCountry);
|
|
137
|
+
const [cardComplete, setCardComplete] = useState(false);
|
|
138
|
+
const [walletCollected, setWalletCollected] = useState<Awaited<ReturnType<typeof collectWallet>> | null>(null);
|
|
139
|
+
const [walletError, setWalletError] = useState<string | null>(null);
|
|
140
|
+
const [walletPresenting, setWalletPresenting] = useState(false);
|
|
141
|
+
|
|
142
|
+
const isWallet = template === 'wallet_sheet';
|
|
143
|
+
const isCard = selected?.category === 'card';
|
|
144
|
+
const complete =
|
|
145
|
+
Boolean(selected) &&
|
|
146
|
+
(isWallet ? Boolean(walletCollected) : isCard ? cardComplete && declared.complete : declared.complete);
|
|
147
|
+
|
|
148
|
+
// A wallet payload is bound to the sheet that produced it, and the sheet was priced. Clearing on
|
|
149
|
+
// the method alone left a token authorized for the OLD amount: `complete` stays true off that
|
|
150
|
+
// stale payload, so the button is disabled and the buyer cannot re-authorize, while confirm
|
|
151
|
+
// submits the old authorization against the new price.
|
|
152
|
+
const walletBinding = `${selectedType ?? ''}|${charge.currency}|${charge.amount}|${options.applePayMerchantId ?? ''}|${options.googlePayMerchantName ?? ''}`;
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
setWalletCollected(null);
|
|
155
|
+
setWalletError(null);
|
|
156
|
+
}, [walletBinding]);
|
|
157
|
+
|
|
158
|
+
// The first offer is selected once the list settles, so a single-method checkout needs no tap.
|
|
159
|
+
// A re-priced charge re-filters the matrix, and the selected method can drop out of it. Left
|
|
160
|
+
// alone the selection points at a type no tile renders: nothing looks selected and confirm
|
|
161
|
+
// refuses. Fall back to the new first offer.
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
if (offers.length === 0) return;
|
|
164
|
+
if (selectedType && offers.some((entry) => entry.type === selectedType)) return;
|
|
165
|
+
setSelectedType(offers[0]!.type);
|
|
166
|
+
}, [offers, selectedType]);
|
|
167
|
+
|
|
168
|
+
// The entry's per-currency country list is the method's billing-country constraint, and "*"
|
|
169
|
+
// anywhere in it means unrestricted. A mounted address element intersects it with the
|
|
170
|
+
// integrator's own allowedCountries, so a method that only serves a few countries narrows the
|
|
171
|
+
// picker instead of letting the buyer choose one it will refuse.
|
|
172
|
+
const methodCountries = useMemo(
|
|
173
|
+
() => (charge.configured ? methodBillingCountries(selected, charge.currency) : null),
|
|
174
|
+
[selected, charge],
|
|
175
|
+
);
|
|
176
|
+
// The controller's mirror of the selection: the confirm dispatcher reads it, and a mounted
|
|
177
|
+
// address element narrows its country selector from it.
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
store.patch({
|
|
180
|
+
selectedType,
|
|
181
|
+
selectedCategory: selected?.category,
|
|
182
|
+
selectedTemplate: template,
|
|
183
|
+
selectedComplete: complete,
|
|
184
|
+
methodCountries,
|
|
185
|
+
});
|
|
186
|
+
}, [store, selectedType, selected?.category, template, complete, methodCountries]);
|
|
187
|
+
|
|
188
|
+
// Unmount only, so the mirror never outlives the surface that owns it: a still-mounted address
|
|
189
|
+
// element would otherwise keep narrowing its picker to a method the buyer can no longer see.
|
|
190
|
+
useEffect(
|
|
191
|
+
() => () =>
|
|
192
|
+
store.patch({
|
|
193
|
+
selectedType: '',
|
|
194
|
+
selectedCategory: undefined,
|
|
195
|
+
selectedTemplate: undefined,
|
|
196
|
+
selectedComplete: false,
|
|
197
|
+
methodCountries: null,
|
|
198
|
+
}),
|
|
199
|
+
[store],
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const latestChange = useRef(onChange);
|
|
203
|
+
latestChange.current = onChange;
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
latestChange.current?.({
|
|
206
|
+
complete,
|
|
207
|
+
type: selected?.type,
|
|
208
|
+
supportsBuyerFee: (selected as { supports_buyer_fee?: boolean } | undefined)?.supports_buyer_fee,
|
|
209
|
+
method: selected
|
|
210
|
+
? {
|
|
211
|
+
type: selected.type,
|
|
212
|
+
category: selected.category,
|
|
213
|
+
template: detailTemplate(selected),
|
|
214
|
+
display_name: selected.display_name,
|
|
215
|
+
}
|
|
216
|
+
: undefined,
|
|
217
|
+
});
|
|
218
|
+
}, [complete, selected]);
|
|
219
|
+
|
|
220
|
+
const walletRequest = useMemo(() => {
|
|
221
|
+
if (!selected || !isWallet || !charge.configured) return null;
|
|
222
|
+
return walletRequestFor(selected, {
|
|
223
|
+
currency: charge.currency,
|
|
224
|
+
amount: charge.amount,
|
|
225
|
+
merchantName: options.googlePayMerchantName ?? 'Whop',
|
|
226
|
+
applePayMerchantId: options.applePayMerchantId,
|
|
227
|
+
requiresBillingAddress: !addressLive,
|
|
228
|
+
requiresEmail: !buyerEmail,
|
|
229
|
+
});
|
|
230
|
+
}, [selected, isWallet, charge, options, addressLive, buyerEmail]);
|
|
231
|
+
|
|
232
|
+
const collect = useCallback(
|
|
233
|
+
async (input: { billingDetails?: Partial<{ email: string; name: string }> }): Promise<CollectedPayment> => {
|
|
234
|
+
if (!selected) {
|
|
235
|
+
throw new PaymentsRefusalError('NO_COLLECTION_SURFACE', 'No payment method is selected');
|
|
236
|
+
}
|
|
237
|
+
if (isWallet) {
|
|
238
|
+
if (!walletCollected) {
|
|
239
|
+
throw new PaymentsRefusalError(
|
|
240
|
+
'WALLET_UNAVAILABLE',
|
|
241
|
+
'Tap the wallet button to authorize the payment before confirming',
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
// The sheet is asked for an email and a billing address exactly when no element is mounted
|
|
245
|
+
// to supply them, so its answers must be folded in BEFORE validation. Validating first
|
|
246
|
+
// rejected a buyer who had just typed their email into Apple Pay.
|
|
247
|
+
return {
|
|
248
|
+
paymentMethod: walletCollected.paymentMethod,
|
|
249
|
+
billingDetails: validateBillingEmail(mergeWalletContact(input?.billingDetails, walletCollected.contact)),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const billingDetails = validateBillingEmail(input?.billingDetails);
|
|
254
|
+
|
|
255
|
+
if (!declared.complete) {
|
|
256
|
+
declared.revealErrors();
|
|
257
|
+
throw new PaymentsRefusalError('NO_COLLECTION_SURFACE', 'Complete the required fields for this method');
|
|
258
|
+
}
|
|
259
|
+
const values = await declared.collect();
|
|
260
|
+
|
|
261
|
+
if (isCard) {
|
|
262
|
+
// The address element supplies the billing NAME, and createConfirmationToken merges it
|
|
263
|
+
// only after this returns. Asserting against the seeded bag therefore rejected the
|
|
264
|
+
// default checkout — an AddressElement and a PaymentElement with no explicit
|
|
265
|
+
// billingDetails — for a name the form had already collected.
|
|
266
|
+
const settled = store.getSnapshot();
|
|
267
|
+
const withAddress = settled.addressLive
|
|
268
|
+
? ((mergeBillingWithCollected(billingDetails, settled.addressCollected) as
|
|
269
|
+
| MintBillingDetails
|
|
270
|
+
| undefined) ?? billingDetails)
|
|
271
|
+
: billingDetails;
|
|
272
|
+
assertCardBilling(withAddress);
|
|
273
|
+
// The card tile mounts its own CardElement, which registers under `card`. Reaching it by
|
|
274
|
+
// kind is required: `liveSurface()` prefers `payment`, which is this very surface.
|
|
275
|
+
const cardSurface = store.surfaceOf('card');
|
|
276
|
+
if (!cardSurface) {
|
|
277
|
+
throw new PaymentsRefusalError('NO_COLLECTION_SURFACE', 'The card fields are not ready');
|
|
278
|
+
}
|
|
279
|
+
const collected = await cardSurface.collect({ billingDetails: withAddress });
|
|
280
|
+
return {
|
|
281
|
+
paymentMethod: { ...collected.paymentMethod, ...values.paymentMethod },
|
|
282
|
+
billingDetails: collected.billingDetails,
|
|
283
|
+
billingExtra: values.billingExtra,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const categoryPayload: Record<string, unknown> = { ...values.paymentMethod };
|
|
288
|
+
if (values.secureTokenIntent) categoryPayload.token_intent = values.secureTokenIntent;
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
paymentMethod: {
|
|
292
|
+
type: selected.type,
|
|
293
|
+
category: selected.category,
|
|
294
|
+
[selected.category]: categoryPayload,
|
|
295
|
+
},
|
|
296
|
+
billingDetails,
|
|
297
|
+
billingExtra: values.billingExtra,
|
|
298
|
+
};
|
|
299
|
+
},
|
|
300
|
+
[selected, isWallet, isCard, walletCollected, declared, store],
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* The sheet's own verdict settles at tokenization, not at confirm.
|
|
305
|
+
*
|
|
306
|
+
* Holding it open until the server answers needs the deferred-conclusion rail the composed
|
|
307
|
+
* checkout arms; a standalone payment element has no such rail on the web either, and its
|
|
308
|
+
* confirm is server-side and unobservable from here. A refused confirm therefore surfaces in the
|
|
309
|
+
* app's own UI rather than in the sheet.
|
|
310
|
+
*/
|
|
311
|
+
const presentWallet = useCallback(async () => {
|
|
312
|
+
if (!selected || !walletRequest || walletPresenting) return;
|
|
313
|
+
setWalletError(null);
|
|
314
|
+
setWalletPresenting(true);
|
|
315
|
+
try {
|
|
316
|
+
setWalletCollected(await collectWallet(selected, walletRequest));
|
|
317
|
+
WhopPlatformPay.complete(true);
|
|
318
|
+
} catch (err) {
|
|
319
|
+
WhopPlatformPay.complete(false);
|
|
320
|
+
const refused = err instanceof PaymentsRefusalError && err.code === 'WALLET_UNAVAILABLE';
|
|
321
|
+
// A dismissal is an outcome, not a failure.
|
|
322
|
+
if (!refused) setWalletError(err instanceof Error ? err.message : String(err));
|
|
323
|
+
} finally {
|
|
324
|
+
setWalletPresenting(false);
|
|
325
|
+
}
|
|
326
|
+
}, [selected, walletRequest, walletPresenting]);
|
|
327
|
+
|
|
328
|
+
// The payment element owns the selection, so it registers ahead of a bare card unit.
|
|
329
|
+
const latestCollect = useRef(collect);
|
|
330
|
+
latestCollect.current = collect;
|
|
331
|
+
useEffect(
|
|
332
|
+
() =>
|
|
333
|
+
store.registerSurface({
|
|
334
|
+
kind: 'payment',
|
|
335
|
+
collect: (input) => latestCollect.current(input),
|
|
336
|
+
}),
|
|
337
|
+
[store],
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
const ready = entries !== null && (walletEntry === undefined || walletAvailable !== undefined);
|
|
341
|
+
|
|
342
|
+
return (
|
|
343
|
+
<ElementFrame
|
|
344
|
+
style={style}
|
|
345
|
+
fallback={fallback}
|
|
346
|
+
skeleton={
|
|
347
|
+
<Stack>
|
|
348
|
+
<Skeleton height={56} />
|
|
349
|
+
<Skeleton height={56} />
|
|
350
|
+
<Skeleton height={56} />
|
|
351
|
+
</Stack>
|
|
352
|
+
}
|
|
353
|
+
ready={ready}
|
|
354
|
+
error={failure}
|
|
355
|
+
onReady={onReady}
|
|
356
|
+
onError={onError}
|
|
357
|
+
>
|
|
358
|
+
{offers.length === 0 && entries !== null ? (
|
|
359
|
+
<View
|
|
360
|
+
style={{
|
|
361
|
+
padding: theme.tokens.spacing * 4,
|
|
362
|
+
borderRadius: theme.tokens.radius,
|
|
363
|
+
backgroundColor: alphaStep(theme.gray, 2),
|
|
364
|
+
}}
|
|
365
|
+
>
|
|
366
|
+
<Text style={{ color: step(theme.gray, 11), fontSize: theme.tokens.fontSize }}>
|
|
367
|
+
{t('No payment methods are available')}
|
|
368
|
+
</Text>
|
|
369
|
+
</View>
|
|
370
|
+
) : (
|
|
371
|
+
<Stack>
|
|
372
|
+
{offers.map((entry) => {
|
|
373
|
+
const active = entry.type === selectedType;
|
|
374
|
+
return (
|
|
375
|
+
<View key={entry.type}>
|
|
376
|
+
<MethodTile
|
|
377
|
+
entry={entry}
|
|
378
|
+
active={active}
|
|
379
|
+
amountLabel={
|
|
380
|
+
charge.configured ? formatMinorAmount(charge.amount, charge.currency, locale) : undefined
|
|
381
|
+
}
|
|
382
|
+
onPress={() => setSelectedType(entry.type)}
|
|
383
|
+
/>
|
|
384
|
+
{active ? (
|
|
385
|
+
<View style={[styles.detail, part(theme, 'whop-PaymentMethodDetail')]}>
|
|
386
|
+
{isCard ? (
|
|
387
|
+
<CardElement onChange={(payload) => setCardComplete(payload.complete)} />
|
|
388
|
+
) : null}
|
|
389
|
+
<DeclaredFields state={declared} />
|
|
390
|
+
{isWallet && walletRequest ? (
|
|
391
|
+
<>
|
|
392
|
+
<WalletPayButton onPress={presentWallet} disabled={walletPresenting || Boolean(walletCollected)} />
|
|
393
|
+
{walletError ? <ErrorFace message={walletError} /> : null}
|
|
394
|
+
</>
|
|
395
|
+
) : null}
|
|
396
|
+
</View>
|
|
397
|
+
) : null}
|
|
398
|
+
</View>
|
|
399
|
+
);
|
|
400
|
+
})}
|
|
401
|
+
</Stack>
|
|
402
|
+
)}
|
|
403
|
+
</ElementFrame>
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function MethodTile({
|
|
408
|
+
entry,
|
|
409
|
+
active,
|
|
410
|
+
amountLabel,
|
|
411
|
+
onPress,
|
|
412
|
+
}: {
|
|
413
|
+
entry: MatrixEntry;
|
|
414
|
+
active: boolean;
|
|
415
|
+
amountLabel: string | undefined;
|
|
416
|
+
onPress: () => void;
|
|
417
|
+
}) {
|
|
418
|
+
const theme = useTheme();
|
|
419
|
+
return (
|
|
420
|
+
<Pressable
|
|
421
|
+
accessibilityRole="radio"
|
|
422
|
+
accessibilityState={{ selected: active }}
|
|
423
|
+
accessibilityLabel={entry.display_name}
|
|
424
|
+
onPress={onPress}
|
|
425
|
+
style={({ pressed }) => [
|
|
426
|
+
styles.tile,
|
|
427
|
+
{
|
|
428
|
+
minHeight: theme.tokens.controlHeight + 12,
|
|
429
|
+
borderRadius: theme.tokens.radius,
|
|
430
|
+
borderWidth: theme.tokens.borderWidth,
|
|
431
|
+
borderColor: active ? step(theme.accent, 8) : alphaStep(theme.gray, 6),
|
|
432
|
+
backgroundColor: pressed ? alphaStep(theme.gray, 2) : step(theme.gray, 1),
|
|
433
|
+
paddingHorizontal: theme.tokens.spacing * 3,
|
|
434
|
+
},
|
|
435
|
+
part(theme, 'whop-PaymentMethodTile'),
|
|
436
|
+
active ? part(theme, 'whop-PaymentMethodRowSelected') : null,
|
|
437
|
+
]}
|
|
438
|
+
>
|
|
439
|
+
<View
|
|
440
|
+
style={[
|
|
441
|
+
styles.radio,
|
|
442
|
+
{ borderColor: active ? step(theme.accent, 9) : alphaStep(theme.gray, 7) },
|
|
443
|
+
]}
|
|
444
|
+
>
|
|
445
|
+
{active ? <View style={[styles.dot, { backgroundColor: step(theme.accent, 9) }]} /> : null}
|
|
446
|
+
</View>
|
|
447
|
+
<PaymentMethodIcon icons={entry.icons} size={20} />
|
|
448
|
+
<Text
|
|
449
|
+
numberOfLines={1}
|
|
450
|
+
style={[
|
|
451
|
+
{
|
|
452
|
+
flexShrink: 1,
|
|
453
|
+
fontSize: theme.tokens.fontSize,
|
|
454
|
+
fontFamily: theme.tokens.fontFamily,
|
|
455
|
+
color: step(theme.gray, 12),
|
|
456
|
+
},
|
|
457
|
+
part(theme, 'whop-MethodTileLabel'),
|
|
458
|
+
]}
|
|
459
|
+
>
|
|
460
|
+
{entry.display_name}
|
|
461
|
+
</Text>
|
|
462
|
+
<View style={styles.spacer} />
|
|
463
|
+
{entry.category === 'card' && !active ? <BrandStrip networks={entry.card?.networks} /> : null}
|
|
464
|
+
{amountLabel ? (
|
|
465
|
+
<Text style={{ fontSize: theme.tokens.labelFontSize, color: step(theme.gray, 11) }}>{amountLabel}</Text>
|
|
466
|
+
) : null}
|
|
467
|
+
</Pressable>
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** The card row's accepted-network art. API art only: an entry without networks renders nothing,
|
|
472
|
+
* the same rule the card fields follow. Tiles overlap left-under-right with the first on top,
|
|
473
|
+
* which is the web strip's stacking without its mask moat (React Native has no mask). */
|
|
474
|
+
function BrandStrip({ networks }: { networks: CardNetworkArt[] | undefined }) {
|
|
475
|
+
const strip = (networks ?? []).slice(0, BRAND_STRIP_TILES);
|
|
476
|
+
if (strip.length === 0) return null;
|
|
477
|
+
return (
|
|
478
|
+
<View style={styles.row}>
|
|
479
|
+
{strip.map((network, index) => (
|
|
480
|
+
<View key={network.brand} style={{ marginLeft: index > 0 ? -8 : 0, zIndex: strip.length - index }}>
|
|
481
|
+
<PaymentMethodIcon icons={network.icons} size={19} />
|
|
482
|
+
</View>
|
|
483
|
+
))}
|
|
484
|
+
</View>
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const BRAND_STRIP_TILES = 4;
|
|
489
|
+
|
|
490
|
+
const styles = StyleSheet.create({
|
|
491
|
+
tile: { flexDirection: 'row', alignItems: 'center', gap: 12 },
|
|
492
|
+
row: { flexDirection: 'row', alignItems: 'center' },
|
|
493
|
+
spacer: { flex: 1 },
|
|
494
|
+
radio: { width: 18, height: 18, borderRadius: 9, borderWidth: 1.5, alignItems: 'center', justifyContent: 'center' },
|
|
495
|
+
dot: { width: 8, height: 8, borderRadius: 4 },
|
|
496
|
+
detail: { paddingTop: 12, gap: 12 },
|
|
497
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { Platform, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { currencyMinorDigits } from '../../core/payments/currency';
|
|
5
|
+
import type { MatrixEntry } from '../../core/payments/detail';
|
|
6
|
+
import { createRawTokenIntent } from '../../core/payments/secure-fields';
|
|
7
|
+
import { PaymentsRefusalError } from '../../core/payments/refusals';
|
|
8
|
+
import { readSheetResult, type WalletContact } from '../../core/payments/wallet-contact';
|
|
9
|
+
import {
|
|
10
|
+
WALLET_TYPE_FOR_PLATFORM,
|
|
11
|
+
WhopPayButton,
|
|
12
|
+
WhopPlatformPay,
|
|
13
|
+
type WalletSheetRequest,
|
|
14
|
+
} from '../../native';
|
|
15
|
+
import { useTheme } from '../../theme/provider';
|
|
16
|
+
|
|
17
|
+
export interface WalletChargeContext {
|
|
18
|
+
currency: string;
|
|
19
|
+
/** Minor units, the way the charge config carries it. */
|
|
20
|
+
amount: number;
|
|
21
|
+
merchantName: string;
|
|
22
|
+
applePayMerchantId: string | undefined;
|
|
23
|
+
requiresBillingAddress: boolean;
|
|
24
|
+
requiresEmail: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* PassKit and the Google Pay API replace the web element's merchant-validation ceremony entirely:
|
|
29
|
+
* the app's own merchant identifier and on-device certificate prove the merchant, so the
|
|
30
|
+
* origin-clearance gates the web wallet lane carries have no counterpart here.
|
|
31
|
+
*/
|
|
32
|
+
export function walletRequestFor(entry: MatrixEntry, charge: WalletChargeContext): WalletSheetRequest | null {
|
|
33
|
+
const wallet = entry.wallet;
|
|
34
|
+
if (!wallet) return null;
|
|
35
|
+
|
|
36
|
+
const merchantIdentifier =
|
|
37
|
+
Platform.OS === 'ios'
|
|
38
|
+
? charge.applePayMerchantId ?? wallet.merchants?.[0]?.merchant_identifier
|
|
39
|
+
: (wallet.gateway_merchant_id ?? '');
|
|
40
|
+
if (Platform.OS === 'ios' && !merchantIdentifier) return null;
|
|
41
|
+
|
|
42
|
+
const countryCode = wallet.merchants?.[0]?.country ?? 'US';
|
|
43
|
+
const major = charge.amount / 10 ** currencyMinorDigits(charge.currency);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
merchantIdentifier: merchantIdentifier ?? '',
|
|
47
|
+
merchantName: charge.merchantName,
|
|
48
|
+
countryCode,
|
|
49
|
+
currencyCode: charge.currency,
|
|
50
|
+
totalPrice: major.toFixed(currencyMinorDigits(charge.currency)),
|
|
51
|
+
lineItems: [{ label: charge.merchantName, amount: major.toFixed(currencyMinorDigits(charge.currency)) }],
|
|
52
|
+
networks: wallet.allowed_card_networks ?? ['visa', 'mastercard', 'amex', 'discover'],
|
|
53
|
+
allowedCardNetworks: wallet.allowed_card_networks,
|
|
54
|
+
allowedAuthMethods: wallet.allowed_auth_methods,
|
|
55
|
+
environment: wallet.environment,
|
|
56
|
+
gateway: wallet.gateway,
|
|
57
|
+
gatewayMerchantId: wallet.gateway_merchant_id,
|
|
58
|
+
requiresBillingAddress: charge.requiresBillingAddress,
|
|
59
|
+
requiresEmail: charge.requiresEmail,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Presents the OS sheet and turns its payload into the confirmation-token payment method.
|
|
65
|
+
*
|
|
66
|
+
* The raw network payload goes straight to Basis Theory as a token intent — byte-identical to what
|
|
67
|
+
* the web wallet lane posts — so the backend sees the same `{ token_intent }` from either platform.
|
|
68
|
+
*/
|
|
69
|
+
export async function collectWallet(
|
|
70
|
+
entry: MatrixEntry,
|
|
71
|
+
request: WalletSheetRequest,
|
|
72
|
+
): Promise<{
|
|
73
|
+
paymentMethod: { type: string; category: string; [key: string]: unknown };
|
|
74
|
+
contact: WalletContact;
|
|
75
|
+
}> {
|
|
76
|
+
const publicKey = entry.wallet?.public_key;
|
|
77
|
+
if (!publicKey) {
|
|
78
|
+
throw new PaymentsRefusalError('WALLET_UNAVAILABLE', 'This wallet has no tokenizer key configured');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const payload = await WhopPlatformPay.present(request);
|
|
82
|
+
if (!payload) {
|
|
83
|
+
throw new PaymentsRefusalError('WALLET_UNAVAILABLE', 'The buyer dismissed the wallet sheet');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const { paymentData, ...contact } = readSheetResult(payload);
|
|
87
|
+
|
|
88
|
+
let data: unknown = paymentData;
|
|
89
|
+
try {
|
|
90
|
+
data = JSON.parse(paymentData);
|
|
91
|
+
} catch {
|
|
92
|
+
// Google Pay hands back an opaque gateway token string rather than JSON.
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const tokenIntent = await createRawTokenIntent(publicKey, data);
|
|
96
|
+
return {
|
|
97
|
+
paymentMethod: {
|
|
98
|
+
type: entry.type,
|
|
99
|
+
category: 'wallet',
|
|
100
|
+
[entry.type]: {
|
|
101
|
+
token_intent: tokenIntent,
|
|
102
|
+
...(Platform.OS === 'ios' ? { merchant_identifier: request.merchantIdentifier } : {}),
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
contact,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
/** Probes whether the OS will present a sheet at all; a false answer hides the tile entirely. */
|
|
112
|
+
export function useWalletAvailability(entry: MatrixEntry | undefined): boolean | undefined {
|
|
113
|
+
const [available, setAvailable] = useState<boolean | undefined>(undefined);
|
|
114
|
+
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
if (!entry) {
|
|
117
|
+
setAvailable(false);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (entry.type !== WALLET_TYPE_FOR_PLATFORM) {
|
|
121
|
+
setAvailable(false);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
let alive = true;
|
|
125
|
+
WhopPlatformPay.canPay({
|
|
126
|
+
networks: entry.wallet?.allowed_card_networks ?? ['visa', 'mastercard', 'amex', 'discover'],
|
|
127
|
+
allowedCardNetworks: entry.wallet?.allowed_card_networks,
|
|
128
|
+
allowedAuthMethods: entry.wallet?.allowed_auth_methods,
|
|
129
|
+
environment: entry.wallet?.environment,
|
|
130
|
+
})
|
|
131
|
+
.then((ok) => alive && setAvailable(ok))
|
|
132
|
+
.catch(() => alive && setAvailable(false));
|
|
133
|
+
return () => {
|
|
134
|
+
alive = false;
|
|
135
|
+
};
|
|
136
|
+
}, [entry]);
|
|
137
|
+
|
|
138
|
+
return available;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function WalletPayButton({ onPress, disabled }: { onPress: () => void; disabled?: boolean }) {
|
|
142
|
+
const theme = useTheme();
|
|
143
|
+
return (
|
|
144
|
+
<View style={{ height: theme.tokens.controlHeight + 4 }}>
|
|
145
|
+
<WhopPayButton
|
|
146
|
+
style={{ flex: 1 }}
|
|
147
|
+
variant="pay"
|
|
148
|
+
buttonAppearance={theme.mode === 'dark' ? 'light' : 'dark'}
|
|
149
|
+
cornerRadius={theme.tokens.radius}
|
|
150
|
+
disabled={disabled}
|
|
151
|
+
onPayPress={onPress}
|
|
152
|
+
/>
|
|
153
|
+
</View>
|
|
154
|
+
);
|
|
155
|
+
}
|