@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,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which React Native files mirror a web elements change.
|
|
3
|
+
*
|
|
4
|
+
* `src/core/` is ported from the web elements, so a fix that lands only on web silently leaves the
|
|
5
|
+
* React Native SDK on the old behaviour. Nothing in a normal web PR points here: the port headers
|
|
6
|
+
* run RN -> web, so an author editing `apps/web/src/elements/payments/lib/currency.ts` sees no sign
|
|
7
|
+
* a second copy exists. This closes that direction.
|
|
8
|
+
*
|
|
9
|
+
* node scripts/mirror.mjs # counterparts for what changed vs origin/main
|
|
10
|
+
* node scripts/mirror.mjs <file>... # counterparts for named web files
|
|
11
|
+
* node scripts/mirror.mjs --validate # every mapping still points at a real file
|
|
12
|
+
*
|
|
13
|
+
* Ported files map themselves: the `ported from <path>` header each one carries IS the edge, so a
|
|
14
|
+
* new port registers by existing. Only the element components need the table below, because they
|
|
15
|
+
* are reimplementations against native primitives rather than ports, and carry no such header.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { execSync } from 'node:child_process';
|
|
19
|
+
import { existsSync, globSync, readFileSync } from 'node:fs';
|
|
20
|
+
import { join, relative } from 'node:path';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
|
|
23
|
+
const root = join(fileURLToPath(new URL('.', import.meta.url)), '..');
|
|
24
|
+
const repo = join(root, '..', '..', '..');
|
|
25
|
+
const WEB = 'frontend/apps/web/src/elements';
|
|
26
|
+
|
|
27
|
+
/** Element components: reimplemented on native primitives, so no port header names the source. */
|
|
28
|
+
const ELEMENTS = {
|
|
29
|
+
'src/elements/payment/index.tsx': [`${WEB}/payments/payment.tsx`],
|
|
30
|
+
'src/elements/payment/declared-fields.tsx': [`${WEB}/payments/payment.tsx`],
|
|
31
|
+
'src/elements/payment/wallet.tsx': [`${WEB}/payments/lib/payment-request.ts`],
|
|
32
|
+
'src/elements/address.tsx': [`${WEB}/payments/address.tsx`],
|
|
33
|
+
'src/elements/card.tsx': [`${WEB}/payments/card.tsx`],
|
|
34
|
+
'src/elements/card-fields/index.tsx': [`${WEB}/payments/card-fields/controller.tsx`],
|
|
35
|
+
'src/elements/card-fields/fields.tsx': [
|
|
36
|
+
`${WEB}/payments/card-fields/card-number.tsx`,
|
|
37
|
+
`${WEB}/payments/card-fields/card-expiry.tsx`,
|
|
38
|
+
`${WEB}/payments/card-fields/card-cvc.tsx`,
|
|
39
|
+
],
|
|
40
|
+
'src/elements/email.tsx': [`${WEB}/payments/email.tsx`],
|
|
41
|
+
'src/elements/tax-id.tsx': [`${WEB}/payments/tax-id.tsx`],
|
|
42
|
+
'src/elements/branding.tsx': [`${WEB}/payments/branding.tsx`],
|
|
43
|
+
'src/controller/actions.ts': [`${WEB}/payments/controller.tsx`],
|
|
44
|
+
'src/controller/store.ts': [`${WEB}/payments/controller.tsx`],
|
|
45
|
+
'src/ui/suggestions.tsx': [`${WEB}/payments/address-suggestions.tsx`],
|
|
46
|
+
'src/ui/payment-method-icon.tsx': [`${WEB}/payments/lib/payment-method-icon.tsx`],
|
|
47
|
+
'src/ui/whop-logo.tsx': [`${WEB}/payments/whop-logo.tsx`],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function portedHeaders() {
|
|
51
|
+
const pairs = {};
|
|
52
|
+
for (const abs of globSync('src/**/*.{ts,tsx}', { cwd: root }).map((p) => join(root, p))) {
|
|
53
|
+
const head = readFileSync(abs, 'utf8').slice(0, 1200);
|
|
54
|
+
const sources = [...head.matchAll(/frontend\/apps\/web\/src\/elements\/[\w\-./]+\.tsx?/g)].map((m) => m[0]);
|
|
55
|
+
if (sources.length > 0) pairs[relative(root, abs)] = [...new Set(sources)];
|
|
56
|
+
}
|
|
57
|
+
return pairs;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function buildMap() {
|
|
61
|
+
const map = { ...portedHeaders() };
|
|
62
|
+
for (const [rn, web] of Object.entries(ELEMENTS)) map[rn] = [...new Set([...(map[rn] ?? []), ...web])];
|
|
63
|
+
return map;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** web path -> the RN files claiming it. */
|
|
67
|
+
function invert(map) {
|
|
68
|
+
const back = new Map();
|
|
69
|
+
for (const [rn, webs] of Object.entries(map)) for (const web of webs) back.set(web, [...(back.get(web) ?? []), rn]);
|
|
70
|
+
return back;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const map = buildMap();
|
|
74
|
+
const args = process.argv.slice(2);
|
|
75
|
+
|
|
76
|
+
if (args.includes('--validate')) {
|
|
77
|
+
const missing = [];
|
|
78
|
+
for (const [rn, webs] of Object.entries(map)) {
|
|
79
|
+
if (!existsSync(join(root, rn))) missing.push(` ${rn} (react native side is gone)`);
|
|
80
|
+
for (const web of webs) if (!existsSync(join(repo, web))) missing.push(` ${rn} -> ${web} (web side is gone)`);
|
|
81
|
+
}
|
|
82
|
+
if (missing.length > 0) {
|
|
83
|
+
console.error('whop elements: mirror map is stale. A file moved or was deleted:\n' + missing.join('\n'));
|
|
84
|
+
console.error('\nFix the port header, or the ELEMENTS table in scripts/mirror.mjs.');
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
console.log(`whop elements: mirror map is valid (${Object.keys(map).length} react native files)`);
|
|
88
|
+
process.exit(0);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const changed =
|
|
92
|
+
args.length > 0
|
|
93
|
+
? args
|
|
94
|
+
: execSync('git diff --name-only origin/main...HEAD', { cwd: repo, encoding: 'utf8' }).split('\n').filter(Boolean);
|
|
95
|
+
|
|
96
|
+
const back = invert(map);
|
|
97
|
+
const hits = changed.flatMap((f) => (back.get(f) ?? []).map((rn) => [f, rn]));
|
|
98
|
+
|
|
99
|
+
if (hits.length === 0) {
|
|
100
|
+
console.log('whop elements: no react native counterpart for the changed web files.');
|
|
101
|
+
process.exit(0);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const byWeb = new Map();
|
|
105
|
+
for (const [web, rn] of hits) byWeb.set(web, [...(byWeb.get(web) ?? []), rn]);
|
|
106
|
+
|
|
107
|
+
console.log('whop elements: these web changes have a React Native counterpart.\n');
|
|
108
|
+
for (const [web, rns] of byWeb) {
|
|
109
|
+
console.log(` ${web}`);
|
|
110
|
+
for (const rn of rns) console.log(` -> packages/elements-react-native/${rn}`);
|
|
111
|
+
}
|
|
112
|
+
console.log('\nPort the change or say why it does not apply. Read .ai-skills/elements-react-native/SKILL.md first.');
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ConfirmationTokenCreateParams,
|
|
3
|
+
ConfirmationTokenResource,
|
|
4
|
+
PageInfo,
|
|
5
|
+
PaymentMethodTypeResource,
|
|
6
|
+
PaymentResource,
|
|
7
|
+
PlanResource,
|
|
8
|
+
} from './types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The API origin is an ENUM over two Whop-owned hosts, never a consumer-supplied string.
|
|
12
|
+
*
|
|
13
|
+
* A consumer-settable origin would let the embedding app point a payment surface at a host it
|
|
14
|
+
* controls and read everything a buyer types into it. The web elements hold the same line
|
|
15
|
+
* (`elements-framework/src/builder/api-base.ts`); this is that rule on React Native.
|
|
16
|
+
*/
|
|
17
|
+
export const API_BASES = {
|
|
18
|
+
production: 'https://api.whop.com',
|
|
19
|
+
sandbox: 'https://api.sandbox.whop.com',
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
export type WhopEnvironment = keyof typeof API_BASES;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Pinned so a server-side schema roll cannot change what a shipped app parses.
|
|
26
|
+
*
|
|
27
|
+
* Must be a REAL released version: the API rejects an unknown date outright, before it even looks
|
|
28
|
+
* at the request. The published list is `Api::Versioning::RELEASED` in
|
|
29
|
+
* backend/lib/api/versioning.rb — bump this to a date from there, never to today's date.
|
|
30
|
+
*/
|
|
31
|
+
const API_VERSION_DATE = '2026-08-25';
|
|
32
|
+
|
|
33
|
+
export class WhopError extends Error {
|
|
34
|
+
readonly status: number;
|
|
35
|
+
/** Alias of `status`; the ported payments error helpers read this name. */
|
|
36
|
+
readonly statusCode: number;
|
|
37
|
+
readonly code: string | undefined;
|
|
38
|
+
readonly body: unknown;
|
|
39
|
+
|
|
40
|
+
constructor(message: string, status: number, code?: string, body?: unknown) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = 'WhopError';
|
|
43
|
+
this.status = status;
|
|
44
|
+
this.statusCode = status;
|
|
45
|
+
this.code = code;
|
|
46
|
+
this.body = body;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface WhopClientOptions {
|
|
51
|
+
environment?: WhopEnvironment;
|
|
52
|
+
/** Resolved per request, so a rotating buyer token is never pinned to boot. */
|
|
53
|
+
getToken?: () => Promise<string | null | undefined> | string | null | undefined;
|
|
54
|
+
/** The Sardine correlation key, stamped as `x-ssk` when a flow surface is armed. */
|
|
55
|
+
getSessionKey?: () => string | null | undefined;
|
|
56
|
+
fetch?: typeof fetch;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface RequestOptions {
|
|
60
|
+
query?: Record<string, string | number | boolean | undefined>;
|
|
61
|
+
body?: unknown;
|
|
62
|
+
token?: string | null;
|
|
63
|
+
/** The recognition probe is IP-throttled, so retrying a 5xx spends a buyer's budget. */
|
|
64
|
+
maxRetries?: number;
|
|
65
|
+
signal?: AbortSignal;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const RETRYABLE = new Set([408, 429, 500, 502, 503, 504]);
|
|
69
|
+
|
|
70
|
+
export class WhopClient {
|
|
71
|
+
readonly baseUrl: string;
|
|
72
|
+
private readonly options: WhopClientOptions;
|
|
73
|
+
private readonly doFetch: typeof fetch;
|
|
74
|
+
|
|
75
|
+
constructor(options: WhopClientOptions = {}) {
|
|
76
|
+
this.options = options;
|
|
77
|
+
this.baseUrl = API_BASES[options.environment ?? 'production'];
|
|
78
|
+
this.doFetch = options.fetch ?? globalThis.fetch;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async request<T>(method: string, path: string, opts: RequestOptions = {}): Promise<T> {
|
|
82
|
+
const url = new URL(`/api/v1${path}`, this.baseUrl);
|
|
83
|
+
for (const [key, value] of Object.entries(opts.query ?? {})) {
|
|
84
|
+
if (value !== undefined) url.searchParams.set(key, String(value));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const token = opts.token !== undefined ? opts.token : await this.options.getToken?.();
|
|
88
|
+
const sessionKey = this.options.getSessionKey?.();
|
|
89
|
+
|
|
90
|
+
const headers: Record<string, string> = {
|
|
91
|
+
accept: 'application/json',
|
|
92
|
+
'api-version-date': API_VERSION_DATE,
|
|
93
|
+
};
|
|
94
|
+
if (opts.body !== undefined) headers['content-type'] = 'application/json';
|
|
95
|
+
if (token) headers.authorization = `Bearer ${token}`;
|
|
96
|
+
if (sessionKey) headers['x-ssk'] = sessionKey;
|
|
97
|
+
|
|
98
|
+
const maxRetries = opts.maxRetries ?? 2;
|
|
99
|
+
let lastError: unknown;
|
|
100
|
+
|
|
101
|
+
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
|
|
102
|
+
let response: Response;
|
|
103
|
+
try {
|
|
104
|
+
response = await this.doFetch(url.toString(), {
|
|
105
|
+
method,
|
|
106
|
+
headers,
|
|
107
|
+
body: opts.body === undefined ? undefined : JSON.stringify(opts.body),
|
|
108
|
+
signal: opts.signal,
|
|
109
|
+
});
|
|
110
|
+
} catch (err) {
|
|
111
|
+
lastError = new WhopError(
|
|
112
|
+
err instanceof Error ? err.message : 'the network request failed',
|
|
113
|
+
0,
|
|
114
|
+
'NETWORK_UNAVAILABLE',
|
|
115
|
+
);
|
|
116
|
+
if (attempt === maxRetries) throw lastError;
|
|
117
|
+
await backoff(attempt);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (response.ok) {
|
|
122
|
+
if (response.status === 204) return undefined as T;
|
|
123
|
+
return (await response.json()) as T;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const body = await response.json().catch(() => undefined);
|
|
127
|
+
const error = new WhopError(errorMessage(body, response.status), response.status, errorCode(body), body);
|
|
128
|
+
if (!RETRYABLE.has(response.status) || attempt === maxRetries) throw error;
|
|
129
|
+
lastError = error;
|
|
130
|
+
await backoff(attempt);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
throw lastError instanceof Error ? lastError : new WhopError('the request failed', 0);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
readonly plans = {
|
|
137
|
+
retrieve: (id: string): Promise<PlanResource> => this.request('GET', `/plans/${encodeURIComponent(id)}`),
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
readonly paymentMethodTypes = {
|
|
141
|
+
list: (query: {
|
|
142
|
+
account_id?: string;
|
|
143
|
+
currency?: string;
|
|
144
|
+
amount?: number;
|
|
145
|
+
surface?: string;
|
|
146
|
+
}): Promise<{ data: PaymentMethodTypeResource[]; page_info: PageInfo }> =>
|
|
147
|
+
this.request('GET', '/payment_method_types', { query }),
|
|
148
|
+
|
|
149
|
+
retrieve: (type: string, query: { account_id?: string }): Promise<PaymentMethodTypeResource> =>
|
|
150
|
+
this.request('GET', `/payment_method_types/${encodeURIComponent(type)}`, { query }),
|
|
151
|
+
|
|
152
|
+
/** Authorizes a Basis Theory session against the nonce-derived card-assembly container. */
|
|
153
|
+
createSession: (
|
|
154
|
+
type: string,
|
|
155
|
+
body: { account_id: string; nonce: string },
|
|
156
|
+
): Promise<{ object: string; type: string; session: { container: string } }> =>
|
|
157
|
+
this.request('POST', `/payment_method_types/${encodeURIComponent(type)}/session`, { body }),
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
readonly confirmationTokens = {
|
|
161
|
+
create: (body: ConfirmationTokenCreateParams): Promise<ConfirmationTokenResource> =>
|
|
162
|
+
this.request('POST', '/confirmation_tokens', { body, maxRetries: 0 }),
|
|
163
|
+
|
|
164
|
+
retrieve: (id: string): Promise<ConfirmationTokenResource> =>
|
|
165
|
+
this.request('GET', `/confirmation_tokens/${encodeURIComponent(id)}`),
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
readonly payments = {
|
|
169
|
+
/** The client secret both identifies and authorizes, so no payment id is needed. */
|
|
170
|
+
retrieveBySecret: (clientSecret: string): Promise<PaymentResource> =>
|
|
171
|
+
this.request('GET', `/payments/${encodeURIComponent(paymentTagFrom(clientSecret))}`, {
|
|
172
|
+
query: { client_secret: clientSecret },
|
|
173
|
+
}),
|
|
174
|
+
|
|
175
|
+
updateBySecret: (clientSecret: string, body: { return_url?: string }): Promise<PaymentResource> =>
|
|
176
|
+
this.request('PATCH', `/payments/${encodeURIComponent(paymentTagFrom(clientSecret))}`, {
|
|
177
|
+
query: { client_secret: clientSecret },
|
|
178
|
+
body,
|
|
179
|
+
maxRetries: 0,
|
|
180
|
+
}),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** `pay_<tag>_secret_v<n>` — the tag is embedded, which is why the secret is the whole reference. */
|
|
185
|
+
export function paymentTagFrom(clientSecret: string): string {
|
|
186
|
+
const marker = clientSecret.indexOf('_secret_');
|
|
187
|
+
return marker === -1 ? clientSecret : clientSecret.slice(0, marker);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function errorMessage(body: unknown, status: number): string {
|
|
191
|
+
const detail = (body as { error?: { message?: string }; message?: string } | undefined) ?? undefined;
|
|
192
|
+
return detail?.error?.message ?? detail?.message ?? `the request failed with status ${status}`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function errorCode(body: unknown): string | undefined {
|
|
196
|
+
const detail = (body as { error?: { type?: string; code?: string } } | undefined) ?? undefined;
|
|
197
|
+
return detail?.error?.code ?? detail?.error?.type;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function backoff(attempt: number): Promise<void> {
|
|
201
|
+
return new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 250));
|
|
202
|
+
}
|
package/src/api/types.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The slice of Whop's REST `/api/v1` surface the payments elements actually read, typed by hand.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately not `@whop/sdk`: that client is generated for the web and carries a transport this
|
|
5
|
+
* package does not want. The field names here are the wire names, so a payload built from these
|
|
6
|
+
* types is the payload the backend expects.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type Money = { amount: number; currency: string };
|
|
10
|
+
|
|
11
|
+
/** The API's own `enabled` / `disabled` / `include_platform_defaults` filtering shape. */
|
|
12
|
+
export interface PaymentMethodConfigurationShape {
|
|
13
|
+
enabled?: string[];
|
|
14
|
+
disabled?: string[];
|
|
15
|
+
include_platform_defaults?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PlanResource {
|
|
19
|
+
id: string;
|
|
20
|
+
object: 'plan';
|
|
21
|
+
currency: string;
|
|
22
|
+
/** MAJOR units, decimal. `chargeFromPlan` converts, truncating the way the backend does. */
|
|
23
|
+
initial_price?: number | null;
|
|
24
|
+
plan_type?: string | null;
|
|
25
|
+
account?: { id?: string } | null;
|
|
26
|
+
/**
|
|
27
|
+
* The GOVERNING configuration, already resolved server-side through every layer. `null` means
|
|
28
|
+
* platform defaults; there is deliberately no client-side fallback to the plan's own override.
|
|
29
|
+
*/
|
|
30
|
+
effective_payment_method_configuration?: PaymentMethodConfigurationShape | null;
|
|
31
|
+
trial_period_days?: number | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface CountryBound {
|
|
35
|
+
country: string;
|
|
36
|
+
min_amount: number | null;
|
|
37
|
+
max_amount: number | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface CurrencyBound {
|
|
41
|
+
min_amount: number | null;
|
|
42
|
+
max_amount: number | null;
|
|
43
|
+
countries: CountryBound[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface RequiredField {
|
|
47
|
+
name: string;
|
|
48
|
+
kind: 'billing_details' | 'payment_method';
|
|
49
|
+
placeholder?: string;
|
|
50
|
+
secure?: boolean;
|
|
51
|
+
/** Country-keyed validation patterns, with `*` as the fallback. */
|
|
52
|
+
formats?: Record<string, string>;
|
|
53
|
+
input_formats?: Record<string, string>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface MethodDetail {
|
|
57
|
+
/** `none` | `redirect` | `bank_wire` | `wallet_sheet` */
|
|
58
|
+
template: string;
|
|
59
|
+
icon?: string;
|
|
60
|
+
short_name?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* One colorway of one icon shape. The SVG is the API's preferred file, but React Native's `Image`
|
|
65
|
+
* renders no vectors and every SVG library that could is a native peer this package refuses to
|
|
66
|
+
* take on, so the raster set is what the elements draw.
|
|
67
|
+
*/
|
|
68
|
+
export interface PaymentMethodIconFiles {
|
|
69
|
+
svg?: string;
|
|
70
|
+
/** The shape's native size: 32px square, 48x30 card. */
|
|
71
|
+
png_1x?: string;
|
|
72
|
+
png_2x?: string;
|
|
73
|
+
png_4x?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface PaymentMethodIconVariants {
|
|
77
|
+
light?: PaymentMethodIconFiles | null;
|
|
78
|
+
dark?: PaymentMethodIconFiles | null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface PaymentMethodIcons {
|
|
82
|
+
/** The square tile (32x32). */
|
|
83
|
+
square?: PaymentMethodIconVariants | null;
|
|
84
|
+
/** The credit-card-proportioned tile (48x30). */
|
|
85
|
+
card?: PaymentMethodIconVariants | null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface CardNetworkArt {
|
|
89
|
+
brand: string;
|
|
90
|
+
display_name?: string;
|
|
91
|
+
icons?: PaymentMethodIcons;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface PaymentMethodTypeResource {
|
|
95
|
+
object: 'payment_method_type';
|
|
96
|
+
type: string;
|
|
97
|
+
category: 'card' | 'wallet' | 'redirect' | 'bank_debit' | 'bank_transfer' | 'voucher' | 'crypto' | 'balance';
|
|
98
|
+
display_name: string;
|
|
99
|
+
icons?: PaymentMethodIcons;
|
|
100
|
+
detail: MethodDetail;
|
|
101
|
+
required_fields?: RequiredField[];
|
|
102
|
+
currencies?: Record<string, CurrencyBound>;
|
|
103
|
+
platform_default?: boolean;
|
|
104
|
+
usage?: { one_time?: boolean; off_session?: boolean; on_session?: boolean };
|
|
105
|
+
card?: { public_key?: string | null; networks?: CardNetworkArt[] } | null;
|
|
106
|
+
wallet?: {
|
|
107
|
+
public_key?: string | null;
|
|
108
|
+
gateway?: string;
|
|
109
|
+
gateway_merchant_id?: string;
|
|
110
|
+
environment?: string;
|
|
111
|
+
allowed_card_networks?: string[];
|
|
112
|
+
allowed_auth_methods?: string[];
|
|
113
|
+
merchants?: Array<{ merchant_identifier?: string; country?: string; domains?: string[] }>;
|
|
114
|
+
} | null;
|
|
115
|
+
config?: Record<string, unknown> | null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface PageInfo {
|
|
119
|
+
end_cursor: string | null;
|
|
120
|
+
has_next_page: boolean;
|
|
121
|
+
has_previous_page: boolean;
|
|
122
|
+
start_cursor: string | null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface ConfirmationTokenResource {
|
|
126
|
+
id: string;
|
|
127
|
+
object: 'confirmation_token';
|
|
128
|
+
payment_method?: { type: string; category: string } & Record<string, unknown>;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface PaymentResource {
|
|
132
|
+
id: string;
|
|
133
|
+
object: 'payment';
|
|
134
|
+
status: string;
|
|
135
|
+
client_secret?: string;
|
|
136
|
+
next_action?: {
|
|
137
|
+
type: string;
|
|
138
|
+
redirect?: { url: string; return_url?: string | null };
|
|
139
|
+
[key: string]: unknown;
|
|
140
|
+
} | null;
|
|
141
|
+
last_payment_error?: {
|
|
142
|
+
code?: string | null;
|
|
143
|
+
decline_code?: string | null;
|
|
144
|
+
message?: string | null;
|
|
145
|
+
} | null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface BrowserInfo {
|
|
149
|
+
platform?: string;
|
|
150
|
+
color_depth?: number;
|
|
151
|
+
screen_height?: number;
|
|
152
|
+
screen_width?: number;
|
|
153
|
+
language?: string;
|
|
154
|
+
java_enabled?: boolean;
|
|
155
|
+
browser_time_difference?: number;
|
|
156
|
+
user_agent?: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface ConfirmationTokenCreateParams {
|
|
160
|
+
account_id: string;
|
|
161
|
+
payment_method: { type?: string; category: string } & Record<string, unknown>;
|
|
162
|
+
billing_details: Record<string, unknown>;
|
|
163
|
+
return_url?: string;
|
|
164
|
+
setup_future_usage?: 'off_session' | 'on_session';
|
|
165
|
+
browser_info?: BrowserInfo;
|
|
166
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import type { PaymentResource } from '../api/types';
|
|
2
|
+
import { mergeBillingWithCollected } from '../core/payments/billing-merge';
|
|
3
|
+
import { BrandingRequiredError } from '../core/payments/errors';
|
|
4
|
+
import { PaymentsRefusalError } from '../core/payments/refusals';
|
|
5
|
+
import { mintConfirmationToken, type MintBillingDetails, validateBillingEmail } from '../core/payments/mint';
|
|
6
|
+
import { WhopAuthSession } from '../native';
|
|
7
|
+
import type { PaymentsStore } from './store';
|
|
8
|
+
|
|
9
|
+
export interface CreateConfirmationTokenInput {
|
|
10
|
+
billingDetails?: Partial<MintBillingDetails>;
|
|
11
|
+
/** Declared inputs a method requires on `billing_details`. */
|
|
12
|
+
billingExtra?: Record<string, string>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CreateConfirmationTokenResult {
|
|
16
|
+
confirmationToken: string;
|
|
17
|
+
type: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The one mint. Resolves the live collection surface, merges the billing block underneath any
|
|
22
|
+
* explicit input, and posts `POST /confirmation_tokens`.
|
|
23
|
+
*/
|
|
24
|
+
export async function createConfirmationToken(
|
|
25
|
+
store: PaymentsStore,
|
|
26
|
+
input: CreateConfirmationTokenInput = {},
|
|
27
|
+
): Promise<CreateConfirmationTokenResult> {
|
|
28
|
+
const state = store.getSnapshot();
|
|
29
|
+
|
|
30
|
+
if (state.confirmInFlight) {
|
|
31
|
+
throw new PaymentsRefusalError(
|
|
32
|
+
'CONFIRMATION_IN_FLIGHT',
|
|
33
|
+
'A confirmation token is already being created for this handle.',
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
// Whop is the merchant of record on these sales, so the buyer has to be told so on the screen
|
|
37
|
+
// where they pay. The web element cannot demand this of already-shipped integrations; a first
|
|
38
|
+
// release can, and does.
|
|
39
|
+
if (!state.brandingMounted) {
|
|
40
|
+
throw new BrandingRequiredError(
|
|
41
|
+
'BRANDING_NOT_MOUNTED',
|
|
42
|
+
'Mount <BrandingElement /> alongside your payment surface — it carries the merchant-of-record notice.',
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
if (!state.charge.configured) {
|
|
46
|
+
throw new PaymentsRefusalError(
|
|
47
|
+
'CHARGE_CONFIG_MISSING',
|
|
48
|
+
'This handle has no charge config — pass a plan or currency and amount.',
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const surface = store.liveSurface();
|
|
53
|
+
if (!surface) {
|
|
54
|
+
throw new PaymentsRefusalError(
|
|
55
|
+
'NO_COLLECTION_SURFACE',
|
|
56
|
+
'No collection surface is mounted — render <PaymentElement />, <CardElement />, or <CardFields>.',
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
store.patch({ confirmInFlight: true });
|
|
61
|
+
try {
|
|
62
|
+
const seeded = {
|
|
63
|
+
...input.billingDetails,
|
|
64
|
+
email: input.billingDetails?.email ?? state.buyerEmail,
|
|
65
|
+
};
|
|
66
|
+
const collected = await surface.collect({ billingDetails: seeded });
|
|
67
|
+
|
|
68
|
+
// Re-read AFTER collect: card and wallet collection take seconds, and a buyer editing their
|
|
69
|
+
// address or email meanwhile must have the live values minted, not the pre-collect snapshot.
|
|
70
|
+
const settled = store.getSnapshot();
|
|
71
|
+
const email = input.billingDetails?.email ?? settled.buyerEmail ?? seeded.email;
|
|
72
|
+
|
|
73
|
+
const billing = mergeBillingWithCollected(
|
|
74
|
+
{ ...collected.billingDetails, ...input.billingDetails, email: email || collected.billingDetails.email },
|
|
75
|
+
settled.addressLive ? settled.addressCollected : undefined,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
return await mintConfirmationToken(
|
|
79
|
+
store.client,
|
|
80
|
+
{
|
|
81
|
+
accountId: settled.accountId,
|
|
82
|
+
returnUrl: settled.returnUrl,
|
|
83
|
+
setupFutureUsage: settled.charge.configured ? settled.charge.setupFutureUsage : undefined,
|
|
84
|
+
},
|
|
85
|
+
collected.paymentMethod,
|
|
86
|
+
validateBillingEmail(billing),
|
|
87
|
+
{ ...collected.billingExtra, ...input.billingExtra },
|
|
88
|
+
);
|
|
89
|
+
} finally {
|
|
90
|
+
store.patch({ confirmInFlight: false });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface HandleNextActionInput {
|
|
95
|
+
/** Identifies AND authorizes the payment — its tag is embedded in the secret. */
|
|
96
|
+
clientSecret: string;
|
|
97
|
+
returnUrl?: string;
|
|
98
|
+
/** @default 2500 */
|
|
99
|
+
pollIntervalMs?: number;
|
|
100
|
+
/** @default 300000 */
|
|
101
|
+
timeoutMs?: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface HandleNextActionResult {
|
|
105
|
+
status: string;
|
|
106
|
+
redirected: boolean;
|
|
107
|
+
lastPaymentError: PaymentResource['last_payment_error'];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const RESTING = new Set(['succeeded', 'failed', 'canceled', 'cancelled', 'requires_payment_method']);
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Completes a confirmed payment's pending step.
|
|
114
|
+
*
|
|
115
|
+
* The redirect leg opens the system browser and then **polls the payment to rest**. Polling is the
|
|
116
|
+
* authority rather than a callback because the API refuses anything but https or loopback for a
|
|
117
|
+
* return URL (`PaymentsApi::ValidateReturnUrl`), so a custom-scheme deep link back into the app is
|
|
118
|
+
* not available. Dismissing the browser therefore returns the payment's current status, which may
|
|
119
|
+
* still be pending — the same contract the web action documents.
|
|
120
|
+
*/
|
|
121
|
+
export async function handleNextAction(
|
|
122
|
+
store: PaymentsStore,
|
|
123
|
+
input: HandleNextActionInput,
|
|
124
|
+
): Promise<HandleNextActionResult> {
|
|
125
|
+
// One at a time, like the mint. Two overlapping calls each open a system browser and each run
|
|
126
|
+
// their own poll, and whichever finishes first calls dismiss() — closing the OTHER buyer-facing
|
|
127
|
+
// challenge mid-flow and orphaning its presentation. A double-tapped submit is enough to do it.
|
|
128
|
+
if (store.getSnapshot().nextActionInFlight) {
|
|
129
|
+
throw new PaymentsRefusalError(
|
|
130
|
+
'NEXT_ACTION_IN_FLIGHT',
|
|
131
|
+
'A payment step is already being handled for this handle.',
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
store.patch({ nextActionInFlight: true });
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
return await runNextAction(store, input);
|
|
138
|
+
} finally {
|
|
139
|
+
store.patch({ nextActionInFlight: false });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function runNextAction(
|
|
144
|
+
store: PaymentsStore,
|
|
145
|
+
input: HandleNextActionInput,
|
|
146
|
+
): Promise<HandleNextActionResult> {
|
|
147
|
+
const pollIntervalMs = input.pollIntervalMs ?? 2500;
|
|
148
|
+
const timeoutMs = input.timeoutMs ?? 300_000;
|
|
149
|
+
|
|
150
|
+
if (input.returnUrl) {
|
|
151
|
+
// A failed update rejects BEFORE any step runs, so the buyer is never sent somewhere the
|
|
152
|
+
// payment does not know about.
|
|
153
|
+
await store.client.payments.updateBySecret(input.clientSecret, { return_url: input.returnUrl });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let payment = await store.client.payments.retrieveBySecret(input.clientSecret);
|
|
157
|
+
const url = payment.next_action?.redirect?.url;
|
|
158
|
+
|
|
159
|
+
if (!payment.next_action || !url) {
|
|
160
|
+
return { status: payment.status, redirected: false, lastPaymentError: payment.last_payment_error ?? null };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// A mutable box rather than a bare `let`: the assignment happens inside a callback, which control
|
|
164
|
+
// flow analysis does not see, so a plain binding narrows to `null` at every read below.
|
|
165
|
+
const presented: { error: Error | null } = { error: null };
|
|
166
|
+
const presentation = WhopAuthSession.present(url).catch((error: unknown) => {
|
|
167
|
+
presented.error = error instanceof Error ? error : new Error(String(error));
|
|
168
|
+
});
|
|
169
|
+
const deadline = Date.now() + timeoutMs;
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
while (Date.now() < deadline) {
|
|
173
|
+
await delay(pollIntervalMs);
|
|
174
|
+
// The buyer cannot be completing a step that never opened. Without this the poll runs the
|
|
175
|
+
// full timeout and then reports a redirect that never happened.
|
|
176
|
+
if (presented.error) break;
|
|
177
|
+
payment = await store.client.payments.retrieveBySecret(input.clientSecret).catch(() => payment);
|
|
178
|
+
if (RESTING.has(payment.status) || !payment.next_action) break;
|
|
179
|
+
}
|
|
180
|
+
} finally {
|
|
181
|
+
WhopAuthSession.dismiss();
|
|
182
|
+
await presentation;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (presented.error) {
|
|
186
|
+
throw new PaymentsRefusalError(
|
|
187
|
+
'NEXT_ACTION_PRESENTATION_FAILED',
|
|
188
|
+
`The payment step could not be opened: ${presented.error.message}`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return { status: payment.status, redirected: true, lastPaymentError: payment.last_payment_error ?? null };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function delay(ms: number): Promise<void> {
|
|
196
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
197
|
+
}
|