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