@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.
Files changed (339) hide show
  1. package/README.md +179 -0
  2. package/android/build.gradle +64 -0
  3. package/android/consumer-rules.pro +3 -0
  4. package/android/gradle.properties +5 -0
  5. package/android/src/main/java/com/whop/elements/WhopAddressSuggestionsModule.kt +102 -0
  6. package/android/src/main/java/com/whop/elements/WhopAuthSessionModule.kt +126 -0
  7. package/android/src/main/java/com/whop/elements/WhopElementsPackage.kt +39 -0
  8. package/android/src/main/java/com/whop/elements/WhopPayButtonManager.kt +74 -0
  9. package/android/src/main/java/com/whop/elements/WhopPayButtonPressEvent.kt +17 -0
  10. package/android/src/main/java/com/whop/elements/WhopPayButtonView.kt +178 -0
  11. package/android/src/main/java/com/whop/elements/WhopPlatformPayModule.kt +212 -0
  12. package/assets/whop-wordmark-dark.png +0 -0
  13. package/assets/whop-wordmark-dark@2x.png +0 -0
  14. package/assets/whop-wordmark-dark@3x.png +0 -0
  15. package/assets/whop-wordmark-light.png +0 -0
  16. package/assets/whop-wordmark-light@2x.png +0 -0
  17. package/assets/whop-wordmark-light@3x.png +0 -0
  18. package/ios/Package.swift +59 -0
  19. package/ios/Sources/ElementsReactNative/WhopAddressSuggestionsImpl.swift +189 -0
  20. package/ios/Sources/ElementsReactNative/WhopAuthSessionImpl.swift +106 -0
  21. package/ios/Sources/ElementsReactNative/WhopPayButtonImpl.swift +89 -0
  22. package/ios/Sources/ElementsReactNative/WhopPlatformPayImpl.swift +181 -0
  23. package/ios/generated/WhopAddressSuggestionsModule.h +11 -0
  24. package/ios/generated/WhopAddressSuggestionsModule.mm +46 -0
  25. package/ios/generated/WhopAuthSessionModule.h +11 -0
  26. package/ios/generated/WhopAuthSessionModule.mm +45 -0
  27. package/ios/generated/WhopElementsShims.h +35 -0
  28. package/ios/generated/WhopPayButtonComponentView.h +12 -0
  29. package/ios/generated/WhopPayButtonComponentView.mm +82 -0
  30. package/ios/generated/WhopPlatformPayModule.h +11 -0
  31. package/ios/generated/WhopPlatformPayModule.mm +51 -0
  32. package/lib/module/api/client.js +139 -0
  33. package/lib/module/api/client.js.map +1 -0
  34. package/lib/module/api/client.test.js +137 -0
  35. package/lib/module/api/client.test.js.map +1 -0
  36. package/lib/module/api/types.js +2 -0
  37. package/lib/module/api/types.js.map +1 -0
  38. package/lib/module/controller/actions.js +148 -0
  39. package/lib/module/controller/actions.js.map +1 -0
  40. package/lib/module/controller/actions.test.js +102 -0
  41. package/lib/module/controller/actions.test.js.map +1 -0
  42. package/lib/module/controller/store.js +158 -0
  43. package/lib/module/controller/store.js.map +1 -0
  44. package/lib/module/core/address/country-data.js +1370 -0
  45. package/lib/module/core/address/country-data.js.map +1 -0
  46. package/lib/module/core/address/country-name.js +38 -0
  47. package/lib/module/core/address/country-name.js.map +1 -0
  48. package/lib/module/core/address/country-names.generated.js +249 -0
  49. package/lib/module/core/address/country-names.generated.js.map +1 -0
  50. package/lib/module/core/address/engine.js +245 -0
  51. package/lib/module/core/address/engine.js.map +1 -0
  52. package/lib/module/core/address/engine.test.js +216 -0
  53. package/lib/module/core/address/engine.test.js.map +1 -0
  54. package/lib/module/core/address/form-state.js +409 -0
  55. package/lib/module/core/address/form-state.js.map +1 -0
  56. package/lib/module/core/address/form-state.test.js +516 -0
  57. package/lib/module/core/address/form-state.test.js.map +1 -0
  58. package/lib/module/core/address/places.js +36 -0
  59. package/lib/module/core/address/places.js.map +1 -0
  60. package/lib/module/core/address/required-coverage.test.js +57 -0
  61. package/lib/module/core/address/required-coverage.test.js.map +1 -0
  62. package/lib/module/core/address/suggest.js +102 -0
  63. package/lib/module/core/address/suggest.js.map +1 -0
  64. package/lib/module/core/address/suggest.test.js +156 -0
  65. package/lib/module/core/address/suggest.test.js.map +1 -0
  66. package/lib/module/core/address/use-suggestions.js +33 -0
  67. package/lib/module/core/address/use-suggestions.js.map +1 -0
  68. package/lib/module/core/payments/address-change.js +91 -0
  69. package/lib/module/core/payments/address-change.js.map +1 -0
  70. package/lib/module/core/payments/billing-merge.js +62 -0
  71. package/lib/module/core/payments/billing-merge.js.map +1 -0
  72. package/lib/module/core/payments/browser-info.js +42 -0
  73. package/lib/module/core/payments/browser-info.js.map +1 -0
  74. package/lib/module/core/payments/bt.js +151 -0
  75. package/lib/module/core/payments/bt.js.map +1 -0
  76. package/lib/module/core/payments/bt.test.js +23 -0
  77. package/lib/module/core/payments/bt.test.js.map +1 -0
  78. package/lib/module/core/payments/currency.js +37 -0
  79. package/lib/module/core/payments/currency.js.map +1 -0
  80. package/lib/module/core/payments/detail.js +159 -0
  81. package/lib/module/core/payments/detail.js.map +1 -0
  82. package/lib/module/core/payments/detail.test.js +225 -0
  83. package/lib/module/core/payments/detail.test.js.map +1 -0
  84. package/lib/module/core/payments/errors.js +115 -0
  85. package/lib/module/core/payments/errors.js.map +1 -0
  86. package/lib/module/core/payments/mint.js +89 -0
  87. package/lib/module/core/payments/mint.js.map +1 -0
  88. package/lib/module/core/payments/plan.js +131 -0
  89. package/lib/module/core/payments/plan.js.map +1 -0
  90. package/lib/module/core/payments/refusals.js +17 -0
  91. package/lib/module/core/payments/refusals.js.map +1 -0
  92. package/lib/module/core/payments/secure-fields.js +87 -0
  93. package/lib/module/core/payments/secure-fields.js.map +1 -0
  94. package/lib/module/core/payments/tax-id-catalog.js +616 -0
  95. package/lib/module/core/payments/tax-id-catalog.js.map +1 -0
  96. package/lib/module/core/payments/wallet-contact.js +42 -0
  97. package/lib/module/core/payments/wallet-contact.js.map +1 -0
  98. package/lib/module/core/payments/wallet-contact.test.js +77 -0
  99. package/lib/module/core/payments/wallet-contact.test.js.map +1 -0
  100. package/lib/module/elements/address.js +467 -0
  101. package/lib/module/elements/address.js.map +1 -0
  102. package/lib/module/elements/branding.js +114 -0
  103. package/lib/module/elements/branding.js.map +1 -0
  104. package/lib/module/elements/card-fields/context.js +254 -0
  105. package/lib/module/elements/card-fields/context.js.map +1 -0
  106. package/lib/module/elements/card-fields/fields.js +309 -0
  107. package/lib/module/elements/card-fields/fields.js.map +1 -0
  108. package/lib/module/elements/card-fields/index.js +30 -0
  109. package/lib/module/elements/card-fields/index.js.map +1 -0
  110. package/lib/module/elements/card.js +107 -0
  111. package/lib/module/elements/card.js.map +1 -0
  112. package/lib/module/elements/email.js +84 -0
  113. package/lib/module/elements/email.js.map +1 -0
  114. package/lib/module/elements/payment/declared-fields.js +186 -0
  115. package/lib/module/elements/payment/declared-fields.js.map +1 -0
  116. package/lib/module/elements/payment/index.js +448 -0
  117. package/lib/module/elements/payment/index.js.map +1 -0
  118. package/lib/module/elements/payment/wallet.js +132 -0
  119. package/lib/module/elements/payment/wallet.js.map +1 -0
  120. package/lib/module/elements/tax-id.js +100 -0
  121. package/lib/module/elements/tax-id.js.map +1 -0
  122. package/lib/module/i18n/catalogs.js +103 -0
  123. package/lib/module/i18n/catalogs.js.map +1 -0
  124. package/lib/module/i18n/index.js +45 -0
  125. package/lib/module/i18n/index.js.map +1 -0
  126. package/lib/module/index.js +27 -0
  127. package/lib/module/index.js.map +1 -0
  128. package/lib/module/namespace.js +174 -0
  129. package/lib/module/namespace.js.map +1 -0
  130. package/lib/module/native/index.js +128 -0
  131. package/lib/module/native/index.js.map +1 -0
  132. package/lib/module/package.json +1 -0
  133. package/lib/module/provider.js +56 -0
  134. package/lib/module/provider.js.map +1 -0
  135. package/lib/module/specs/NativeWhopAddressSuggestions.js +6 -0
  136. package/lib/module/specs/NativeWhopAddressSuggestions.js.map +1 -0
  137. package/lib/module/specs/NativeWhopAuthSession.js +10 -0
  138. package/lib/module/specs/NativeWhopAuthSession.js.map +1 -0
  139. package/lib/module/specs/NativeWhopPlatformPay.js +10 -0
  140. package/lib/module/specs/NativeWhopPlatformPay.js.map +1 -0
  141. package/lib/module/specs/WhopPayButtonNativeComponent.ts +22 -0
  142. package/lib/module/theme/appearance.js +85 -0
  143. package/lib/module/theme/appearance.js.map +1 -0
  144. package/lib/module/theme/appearance.test.js +124 -0
  145. package/lib/module/theme/appearance.test.js.map +1 -0
  146. package/lib/module/theme/provider.js +25 -0
  147. package/lib/module/theme/provider.js.map +1 -0
  148. package/lib/module/theme/resolve.js +69 -0
  149. package/lib/module/theme/resolve.js.map +1 -0
  150. package/lib/module/theme/scales.generated.js +419 -0
  151. package/lib/module/theme/scales.generated.js.map +1 -0
  152. package/lib/module/ui/element-frame.js +83 -0
  153. package/lib/module/ui/element-frame.js.map +1 -0
  154. package/lib/module/ui/payment-method-icon.js +69 -0
  155. package/lib/module/ui/payment-method-icon.js.map +1 -0
  156. package/lib/module/ui/primitives.js +266 -0
  157. package/lib/module/ui/primitives.js.map +1 -0
  158. package/lib/module/ui/select.js +281 -0
  159. package/lib/module/ui/select.js.map +1 -0
  160. package/lib/module/ui/suggestions.js +71 -0
  161. package/lib/module/ui/suggestions.js.map +1 -0
  162. package/lib/module/ui/whop-logo.js +42 -0
  163. package/lib/module/ui/whop-logo.js.map +1 -0
  164. package/lib/typescript/package.json +1 -0
  165. package/lib/typescript/src/api/client.d.ts +87 -0
  166. package/lib/typescript/src/api/client.d.ts.map +1 -0
  167. package/lib/typescript/src/api/types.d.ts +175 -0
  168. package/lib/typescript/src/api/types.d.ts.map +1 -0
  169. package/lib/typescript/src/controller/actions.d.ts +42 -0
  170. package/lib/typescript/src/controller/actions.d.ts.map +1 -0
  171. package/lib/typescript/src/controller/store.d.ts +90 -0
  172. package/lib/typescript/src/controller/store.d.ts.map +1 -0
  173. package/lib/typescript/src/core/address/country-data.d.ts +4 -0
  174. package/lib/typescript/src/core/address/country-data.d.ts.map +1 -0
  175. package/lib/typescript/src/core/address/country-name.d.ts +2 -0
  176. package/lib/typescript/src/core/address/country-name.d.ts.map +1 -0
  177. package/lib/typescript/src/core/address/country-names.generated.d.ts +2 -0
  178. package/lib/typescript/src/core/address/country-names.generated.d.ts.map +1 -0
  179. package/lib/typescript/src/core/address/engine.d.ts +97 -0
  180. package/lib/typescript/src/core/address/engine.d.ts.map +1 -0
  181. package/lib/typescript/src/core/address/form-state.d.ts +144 -0
  182. package/lib/typescript/src/core/address/form-state.d.ts.map +1 -0
  183. package/lib/typescript/src/core/address/places.d.ts +23 -0
  184. package/lib/typescript/src/core/address/places.d.ts.map +1 -0
  185. package/lib/typescript/src/core/address/suggest.d.ts +47 -0
  186. package/lib/typescript/src/core/address/suggest.d.ts.map +1 -0
  187. package/lib/typescript/src/core/address/use-suggestions.d.ts +3 -0
  188. package/lib/typescript/src/core/address/use-suggestions.d.ts.map +1 -0
  189. package/lib/typescript/src/core/payments/address-change.d.ts +52 -0
  190. package/lib/typescript/src/core/payments/address-change.d.ts.map +1 -0
  191. package/lib/typescript/src/core/payments/billing-merge.d.ts +39 -0
  192. package/lib/typescript/src/core/payments/billing-merge.d.ts.map +1 -0
  193. package/lib/typescript/src/core/payments/browser-info.d.ts +12 -0
  194. package/lib/typescript/src/core/payments/browser-info.d.ts.map +1 -0
  195. package/lib/typescript/src/core/payments/bt.d.ts +55 -0
  196. package/lib/typescript/src/core/payments/bt.d.ts.map +1 -0
  197. package/lib/typescript/src/core/payments/currency.d.ts +7 -0
  198. package/lib/typescript/src/core/payments/currency.d.ts.map +1 -0
  199. package/lib/typescript/src/core/payments/detail.d.ts +53 -0
  200. package/lib/typescript/src/core/payments/detail.d.ts.map +1 -0
  201. package/lib/typescript/src/core/payments/errors.d.ts +62 -0
  202. package/lib/typescript/src/core/payments/errors.d.ts.map +1 -0
  203. package/lib/typescript/src/core/payments/mint.d.ts +74 -0
  204. package/lib/typescript/src/core/payments/mint.d.ts.map +1 -0
  205. package/lib/typescript/src/core/payments/plan.d.ts +69 -0
  206. package/lib/typescript/src/core/payments/plan.d.ts.map +1 -0
  207. package/lib/typescript/src/core/payments/refusals.d.ts +12 -0
  208. package/lib/typescript/src/core/payments/refusals.d.ts.map +1 -0
  209. package/lib/typescript/src/core/payments/secure-fields.d.ts +33 -0
  210. package/lib/typescript/src/core/payments/secure-fields.d.ts.map +1 -0
  211. package/lib/typescript/src/core/payments/tax-id-catalog.d.ts +481 -0
  212. package/lib/typescript/src/core/payments/tax-id-catalog.d.ts.map +1 -0
  213. package/lib/typescript/src/core/payments/wallet-contact.d.ts +40 -0
  214. package/lib/typescript/src/core/payments/wallet-contact.d.ts.map +1 -0
  215. package/lib/typescript/src/elements/address.d.ts +55 -0
  216. package/lib/typescript/src/elements/address.d.ts.map +1 -0
  217. package/lib/typescript/src/elements/branding.d.ts +18 -0
  218. package/lib/typescript/src/elements/branding.d.ts.map +1 -0
  219. package/lib/typescript/src/elements/card-fields/context.d.ts +67 -0
  220. package/lib/typescript/src/elements/card-fields/context.d.ts.map +1 -0
  221. package/lib/typescript/src/elements/card-fields/fields.d.ts +15 -0
  222. package/lib/typescript/src/elements/card-fields/fields.d.ts.map +1 -0
  223. package/lib/typescript/src/elements/card-fields/index.d.ts +21 -0
  224. package/lib/typescript/src/elements/card-fields/index.d.ts.map +1 -0
  225. package/lib/typescript/src/elements/card.d.ts +21 -0
  226. package/lib/typescript/src/elements/card.d.ts.map +1 -0
  227. package/lib/typescript/src/elements/email.d.ts +31 -0
  228. package/lib/typescript/src/elements/email.d.ts.map +1 -0
  229. package/lib/typescript/src/elements/payment/declared-fields.d.ts +43 -0
  230. package/lib/typescript/src/elements/payment/declared-fields.d.ts.map +1 -0
  231. package/lib/typescript/src/elements/payment/index.d.ts +37 -0
  232. package/lib/typescript/src/elements/payment/index.d.ts.map +1 -0
  233. package/lib/typescript/src/elements/payment/wallet.d.ts +39 -0
  234. package/lib/typescript/src/elements/payment/wallet.d.ts.map +1 -0
  235. package/lib/typescript/src/elements/tax-id.d.ts +41 -0
  236. package/lib/typescript/src/elements/tax-id.d.ts.map +1 -0
  237. package/lib/typescript/src/i18n/catalogs.d.ts +7 -0
  238. package/lib/typescript/src/i18n/catalogs.d.ts.map +1 -0
  239. package/lib/typescript/src/i18n/index.d.ts +17 -0
  240. package/lib/typescript/src/i18n/index.d.ts.map +1 -0
  241. package/lib/typescript/src/index.d.ts +42 -0
  242. package/lib/typescript/src/index.d.ts.map +1 -0
  243. package/lib/typescript/src/namespace.d.ts +36 -0
  244. package/lib/typescript/src/namespace.d.ts.map +1 -0
  245. package/lib/typescript/src/native/index.d.ts +62 -0
  246. package/lib/typescript/src/native/index.d.ts.map +1 -0
  247. package/lib/typescript/src/provider.d.ts +37 -0
  248. package/lib/typescript/src/provider.d.ts.map +1 -0
  249. package/lib/typescript/src/specs/NativeWhopAddressSuggestions.d.ts +13 -0
  250. package/lib/typescript/src/specs/NativeWhopAddressSuggestions.d.ts.map +1 -0
  251. package/lib/typescript/src/specs/NativeWhopAuthSession.d.ts +20 -0
  252. package/lib/typescript/src/specs/NativeWhopAuthSession.d.ts.map +1 -0
  253. package/lib/typescript/src/specs/NativeWhopPlatformPay.d.ts +24 -0
  254. package/lib/typescript/src/specs/NativeWhopPlatformPay.d.ts.map +1 -0
  255. package/lib/typescript/src/specs/WhopPayButtonNativeComponent.d.ts +21 -0
  256. package/lib/typescript/src/specs/WhopPayButtonNativeComponent.d.ts.map +1 -0
  257. package/lib/typescript/src/theme/appearance.d.ts +71 -0
  258. package/lib/typescript/src/theme/appearance.d.ts.map +1 -0
  259. package/lib/typescript/src/theme/provider.d.ts +11 -0
  260. package/lib/typescript/src/theme/provider.d.ts.map +1 -0
  261. package/lib/typescript/src/theme/resolve.d.ts +19 -0
  262. package/lib/typescript/src/theme/resolve.d.ts.map +1 -0
  263. package/lib/typescript/src/theme/scales.generated.d.ts +18 -0
  264. package/lib/typescript/src/theme/scales.generated.d.ts.map +1 -0
  265. package/lib/typescript/src/ui/element-frame.d.ts +32 -0
  266. package/lib/typescript/src/ui/element-frame.d.ts.map +1 -0
  267. package/lib/typescript/src/ui/payment-method-icon.d.ts +21 -0
  268. package/lib/typescript/src/ui/payment-method-icon.d.ts.map +1 -0
  269. package/lib/typescript/src/ui/primitives.d.ts +94 -0
  270. package/lib/typescript/src/ui/primitives.d.ts.map +1 -0
  271. package/lib/typescript/src/ui/select.d.ts +25 -0
  272. package/lib/typescript/src/ui/select.d.ts.map +1 -0
  273. package/lib/typescript/src/ui/suggestions.d.ts +15 -0
  274. package/lib/typescript/src/ui/suggestions.d.ts.map +1 -0
  275. package/lib/typescript/src/ui/whop-logo.d.ts +4 -0
  276. package/lib/typescript/src/ui/whop-logo.d.ts.map +1 -0
  277. package/package.json +121 -0
  278. package/scripts/check-swift.sh +27 -0
  279. package/scripts/codegen-shims.mjs +409 -0
  280. package/scripts/gen-country-names.mjs +58 -0
  281. package/scripts/gen-theme.mjs +124 -0
  282. package/scripts/gen-wordmark.mjs +68 -0
  283. package/scripts/mirror.mjs +112 -0
  284. package/src/api/client.ts +202 -0
  285. package/src/api/types.ts +166 -0
  286. package/src/controller/actions.ts +197 -0
  287. package/src/controller/store.ts +201 -0
  288. package/src/core/address/country-data.ts +927 -0
  289. package/src/core/address/country-name.ts +36 -0
  290. package/src/core/address/country-names.generated.ts +246 -0
  291. package/src/core/address/engine.ts +349 -0
  292. package/src/core/address/form-state.ts +479 -0
  293. package/src/core/address/places.ts +39 -0
  294. package/src/core/address/suggest.ts +153 -0
  295. package/src/core/address/use-suggestions.ts +37 -0
  296. package/src/core/payments/address-change.ts +121 -0
  297. package/src/core/payments/billing-merge.ts +91 -0
  298. package/src/core/payments/browser-info.ts +43 -0
  299. package/src/core/payments/bt.ts +176 -0
  300. package/src/core/payments/currency.ts +54 -0
  301. package/src/core/payments/detail.ts +204 -0
  302. package/src/core/payments/errors.ts +171 -0
  303. package/src/core/payments/mint.ts +130 -0
  304. package/src/core/payments/plan.ts +198 -0
  305. package/src/core/payments/refusals.ts +24 -0
  306. package/src/core/payments/secure-fields.ts +85 -0
  307. package/src/core/payments/tax-id-catalog.ts +320 -0
  308. package/src/core/payments/wallet-contact.ts +56 -0
  309. package/src/elements/address.tsx +570 -0
  310. package/src/elements/branding.tsx +87 -0
  311. package/src/elements/card-fields/context.tsx +310 -0
  312. package/src/elements/card-fields/fields.tsx +281 -0
  313. package/src/elements/card-fields/index.tsx +37 -0
  314. package/src/elements/card.tsx +101 -0
  315. package/src/elements/email.tsx +100 -0
  316. package/src/elements/payment/declared-fields.tsx +238 -0
  317. package/src/elements/payment/index.tsx +497 -0
  318. package/src/elements/payment/wallet.tsx +155 -0
  319. package/src/elements/tax-id.tsx +143 -0
  320. package/src/i18n/catalogs.ts +100 -0
  321. package/src/i18n/index.tsx +46 -0
  322. package/src/index.ts +68 -0
  323. package/src/namespace.tsx +231 -0
  324. package/src/native/index.ts +173 -0
  325. package/src/provider.tsx +76 -0
  326. package/src/specs/NativeWhopAddressSuggestions.ts +14 -0
  327. package/src/specs/NativeWhopAuthSession.ts +21 -0
  328. package/src/specs/NativeWhopPlatformPay.ts +25 -0
  329. package/src/specs/WhopPayButtonNativeComponent.ts +22 -0
  330. package/src/theme/appearance.ts +150 -0
  331. package/src/theme/provider.tsx +28 -0
  332. package/src/theme/resolve.ts +68 -0
  333. package/src/theme/scales.generated.ts +427 -0
  334. package/src/ui/element-frame.tsx +94 -0
  335. package/src/ui/payment-method-icon.tsx +55 -0
  336. package/src/ui/primitives.tsx +269 -0
  337. package/src/ui/select.tsx +255 -0
  338. package/src/ui/suggestions.tsx +69 -0
  339. 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
+ }