@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,178 @@
1
+ package com.whop.elements
2
+
3
+ import android.content.Context
4
+ import android.content.res.Configuration
5
+ import android.view.View
6
+ import com.facebook.react.uimanager.ThemedReactContext
7
+ import android.widget.FrameLayout
8
+ import com.google.android.gms.wallet.button.ButtonConstants
9
+ import com.google.android.gms.wallet.button.ButtonOptions
10
+ import com.google.android.gms.wallet.button.PayButton
11
+ import org.json.JSONArray
12
+ import org.json.JSONObject
13
+
14
+ /**
15
+ * Google's own `PayButton`, wrapped rather than subclassed: `PayButton` is `final`, so this holds
16
+ * one as its single child and keeps the prop state beside it.
17
+ *
18
+ * `PayButton` takes its whole configuration as one immutable options bag, so every prop change has
19
+ * to replay the others. The state lives on the view rather than in `setTag`, which requires a real
20
+ * resource id and throws on anything else.
21
+ */
22
+ class WhopPayButtonView(context: Context) : FrameLayout(context) {
23
+
24
+ private var button: PayButton? = null
25
+ private var pressListener: View.OnClickListener? = null
26
+ private var rebuildScheduled = false
27
+
28
+ private var buttonType: Int = ButtonConstants.ButtonType.BUY
29
+ private var buttonTheme: Int = ButtonConstants.ButtonTheme.DARK
30
+ private var radius: Int = DEFAULT_RADIUS
31
+ private var themeFollowsSystem: Boolean = true
32
+ private var disabled: Boolean = false
33
+
34
+ init {
35
+ scheduleRebuild()
36
+ }
37
+
38
+ /** Google routes `PayButton.setOnClickListener` through its own internal handler, so the press
39
+ * must be bound on the button itself rather than on this wrapper. */
40
+ fun setOnPressListener(listener: View.OnClickListener) {
41
+ pressListener = listener
42
+ button?.setOnClickListener(listener)
43
+ }
44
+
45
+ fun setVariant(value: String?) {
46
+ // PayButton offers a small verb set; anything else maps onto BUY, which is what Google's
47
+ // guidelines prescribe for a checkout action.
48
+ buttonType = when (value) {
49
+ "pay" -> ButtonConstants.ButtonType.PAY
50
+ "checkout" -> ButtonConstants.ButtonType.CHECKOUT
51
+ "book" -> ButtonConstants.ButtonType.BOOK
52
+ "subscribe" -> ButtonConstants.ButtonType.SUBSCRIBE
53
+ "order" -> ButtonConstants.ButtonType.ORDER
54
+ else -> ButtonConstants.ButtonType.BUY
55
+ }
56
+ scheduleRebuild()
57
+ }
58
+
59
+ fun setButtonAppearance(value: String?) {
60
+ themeFollowsSystem = value == null || value == "auto"
61
+ buttonTheme = when (value) {
62
+ "light" -> ButtonConstants.ButtonTheme.LIGHT
63
+ "dark" -> ButtonConstants.ButtonTheme.DARK
64
+ else -> systemTheme()
65
+ }
66
+ scheduleRebuild()
67
+ }
68
+
69
+ fun setCornerRadius(value: Double) {
70
+ radius = value.toInt()
71
+ scheduleRebuild()
72
+ }
73
+
74
+ fun setDisabledState(value: Boolean) {
75
+ disabled = value
76
+ applyEnabled()
77
+ }
78
+
79
+ override fun onConfigurationChanged(newConfig: Configuration?) {
80
+ super.onConfigurationChanged(newConfig)
81
+ if (!themeFollowsSystem) return
82
+ buttonTheme = systemTheme()
83
+ scheduleRebuild()
84
+ }
85
+
86
+ private fun systemTheme(): Int =
87
+ if ((resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
88
+ Configuration.UI_MODE_NIGHT_YES
89
+ ) {
90
+ ButtonConstants.ButtonTheme.LIGHT
91
+ } else {
92
+ ButtonConstants.ButtonTheme.DARK
93
+ }
94
+
95
+ private fun applyEnabled() {
96
+ val current = button ?: return
97
+ current.isEnabled = !disabled
98
+ current.alpha = if (disabled) 0.5f else 1f
99
+ }
100
+
101
+ /**
102
+ * `PayButton.initialize` is documented as a once-per-instance call, and Fabric writes every prop
103
+ * separately right after construction. Applying each write in place meant initializing the same
104
+ * button four times before it was ever shown. Coalescing to one pass per frame and handing that
105
+ * pass a FRESH button keeps the contract without having to diff the options bag.
106
+ */
107
+ private fun scheduleRebuild() {
108
+ if (rebuildScheduled) return
109
+ rebuildScheduled = true
110
+ post {
111
+ rebuildScheduled = false
112
+ rebuild()
113
+ }
114
+ }
115
+
116
+ /** PayButton inflates AppCompat widgets, so it is built against the Activity where there is one. */
117
+ private fun hostContext(): Context = (context as? ThemedReactContext)?.currentActivity ?: context
118
+
119
+ private fun rebuild() {
120
+ removeAllViews()
121
+ val next = PayButton(hostContext())
122
+ next.initialize(
123
+ ButtonOptions.newBuilder()
124
+ .setButtonType(buttonType)
125
+ .setButtonTheme(buttonTheme)
126
+ .setCornerRadius(radius)
127
+ .setAllowedPaymentMethods(ALLOWED_METHODS)
128
+ .build(),
129
+ )
130
+ pressListener?.let(next::setOnClickListener)
131
+ button = next
132
+ applyEnabled()
133
+ addView(next, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
134
+ layoutButton()
135
+ }
136
+
137
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
138
+ super.onLayout(changed, left, top, right, bottom)
139
+ layoutButton()
140
+ }
141
+
142
+ /**
143
+ * React Native lays out only the views IT created. A child added here never gets a measure pass,
144
+ * so `PayButton` sat at 0x0 inside a correctly sized wrapper and drew nothing — the button looked
145
+ * like it had failed to load when it had initialized fine. MATCH_PARENT means nothing without
146
+ * this, because nothing ever resolves it.
147
+ */
148
+ private fun layoutButton() {
149
+ val child = button ?: return
150
+ if (width == 0 || height == 0) return
151
+ child.measure(
152
+ MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
153
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY),
154
+ )
155
+ child.layout(0, 0, width, height)
156
+ }
157
+
158
+ companion object {
159
+ private const val DEFAULT_RADIUS = 8
160
+
161
+ private val ALLOWED_METHODS: String =
162
+ JSONArray()
163
+ .put(
164
+ JSONObject()
165
+ .put("type", "CARD")
166
+ .put(
167
+ "parameters",
168
+ JSONObject()
169
+ .put("allowedAuthMethods", JSONArray().put("PAN_ONLY").put("CRYPTOGRAM_3DS"))
170
+ .put(
171
+ "allowedCardNetworks",
172
+ JSONArray().put("VISA").put("MASTERCARD").put("AMEX").put("DISCOVER"),
173
+ ),
174
+ ),
175
+ )
176
+ .toString()
177
+ }
178
+ }
@@ -0,0 +1,212 @@
1
+ package com.whop.elements
2
+
3
+ import android.app.Activity
4
+ import android.content.Intent
5
+ import com.facebook.react.bridge.ActivityEventListener
6
+ import com.facebook.react.bridge.Promise
7
+ import com.facebook.react.bridge.ReactApplicationContext
8
+ import com.facebook.react.module.annotations.ReactModule
9
+ import com.google.android.gms.tasks.Task
10
+ import com.google.android.gms.wallet.AutoResolveHelper
11
+ import com.google.android.gms.wallet.IsReadyToPayRequest
12
+ import com.google.android.gms.wallet.PaymentData
13
+ import com.google.android.gms.wallet.PaymentDataRequest
14
+ import com.google.android.gms.wallet.PaymentsClient
15
+ import com.google.android.gms.wallet.Wallet
16
+ import com.google.android.gms.wallet.WalletConstants
17
+ import org.json.JSONArray
18
+ import org.json.JSONObject
19
+
20
+ /**
21
+ * Google Pay, through the Wallet API.
22
+ *
23
+ * What crosses back is `PaymentData`'s tokenization token — the same shape the web wallet lane posts
24
+ * to Basis Theory as a token intent, so the confirmation-token payload downstream is identical on
25
+ * both platforms.
26
+ */
27
+ @ReactModule(name = WhopPlatformPayModule.NAME)
28
+ class WhopPlatformPayModule(private val reactContext: ReactApplicationContext) :
29
+ NativeWhopPlatformPaySpec(reactContext), ActivityEventListener {
30
+
31
+ private var pending: Promise? = null
32
+
33
+ init {
34
+ reactContext.addActivityEventListener(this)
35
+ }
36
+
37
+ override fun getName(): String = NAME
38
+
39
+ override fun capability(): String = "google"
40
+
41
+ override fun canPay(configJson: String?, promise: Promise) {
42
+ val config = runCatching { JSONObject(configJson ?: "{}") }.getOrNull()
43
+ if (config == null) {
44
+ promise.reject("INVALID_CONFIG", "whop elements: could not decode the wallet config")
45
+ return
46
+ }
47
+ val request = IsReadyToPayRequest.fromJson(
48
+ JSONObject()
49
+ .put("apiVersion", 2)
50
+ .put("apiVersionMinor", 0)
51
+ .put("allowedPaymentMethods", JSONArray().put(baseCardMethod(config)))
52
+ .toString(),
53
+ )
54
+ client(config.optString("environment", "TEST"))
55
+ .isReadyToPay(request)
56
+ .addOnCompleteListener { task: Task<Boolean> ->
57
+ promise.resolve(runCatching { task.getResult(ApiExceptionClass) }.getOrDefault(false))
58
+ }
59
+ }
60
+
61
+ override fun present(requestJson: String?, promise: Promise) {
62
+ val request = runCatching { JSONObject(requestJson ?: "{}") }.getOrNull()
63
+ val activity: Activity? = reactApplicationContext.currentActivity
64
+ if (request == null) {
65
+ promise.reject("INVALID_REQUEST", "whop elements: could not decode the wallet request")
66
+ return
67
+ }
68
+ if (activity == null) {
69
+ promise.reject("NO_ACTIVITY", "whop elements: no activity is attached")
70
+ return
71
+ }
72
+
73
+ pending?.resolve(null)
74
+ pending = promise
75
+
76
+ val payload = JSONObject()
77
+ .put("apiVersion", 2)
78
+ .put("apiVersionMinor", 0)
79
+ .put("allowedPaymentMethods", JSONArray().put(tokenizedCardMethod(request)))
80
+ .put(
81
+ "transactionInfo",
82
+ JSONObject()
83
+ .put("totalPrice", request.optString("totalPrice"))
84
+ .put("totalPriceStatus", "FINAL")
85
+ .put("countryCode", request.optString("countryCode"))
86
+ .put("currencyCode", request.optString("currencyCode").uppercase()),
87
+ )
88
+ .put("merchantInfo", JSONObject().put("merchantName", request.optString("merchantName", "Whop")))
89
+ .put("emailRequired", request.optBoolean("requiresEmail", false))
90
+
91
+ AutoResolveHelper.resolveTask(
92
+ client(request.optString("environment", "TEST"))
93
+ .loadPaymentData(PaymentDataRequest.fromJson(payload.toString())),
94
+ activity,
95
+ LOAD_PAYMENT_DATA,
96
+ )
97
+ }
98
+
99
+ /** Google Pay's sheet dismisses itself on result, so the confirm verdict has nothing to settle. */
100
+ override fun complete(success: Boolean) = Unit
101
+
102
+ override fun onActivityResult(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) {
103
+ if (requestCode != LOAD_PAYMENT_DATA) return
104
+ val promise = pending ?: return
105
+ pending = null
106
+ if (resultCode != Activity.RESULT_OK || data == null) {
107
+ promise.resolve(null)
108
+ return
109
+ }
110
+ val payload = PaymentData.getFromIntent(data)?.toJson()?.let(::JSONObject)
111
+ val token = payload
112
+ ?.optJSONObject("paymentMethodData")
113
+ ?.optJSONObject("tokenizationData")
114
+ ?.optString("token")
115
+ ?.takeIf { it.isNotEmpty() }
116
+ if (token == null || payload == null) {
117
+ promise.resolve(null)
118
+ return
119
+ }
120
+ promise.resolve(envelope(token, payload).toString())
121
+ }
122
+
123
+ /** The token PLUS whatever contact fields the sheet collected, which are otherwise discarded. */
124
+ private fun envelope(token: String, payload: JSONObject): JSONObject {
125
+ val out = JSONObject().put("paymentData", token)
126
+ payload.optString("email").takeIf { it.isNotEmpty() }?.let { out.put("email", it) }
127
+ val billing = payload
128
+ .optJSONObject("paymentMethodData")
129
+ ?.optJSONObject("info")
130
+ ?.optJSONObject("billingAddress")
131
+ ?: return out
132
+
133
+ billing.optString("name").takeIf { it.isNotEmpty() }?.let { out.put("name", it) }
134
+ val address = JSONObject()
135
+ // Google's FULL billing address: address1..address3, locality, administrativeArea, postalCode.
136
+ billing.optString("address1").takeIf { it.isNotEmpty() }?.let { address.put("line1", it) }
137
+ listOf(billing.optString("address2"), billing.optString("address3"))
138
+ .filter { it.isNotEmpty() }
139
+ .takeIf { it.isNotEmpty() }
140
+ ?.let { address.put("line2", it.joinToString(" ")) }
141
+ billing.optString("locality").takeIf { it.isNotEmpty() }?.let { address.put("city", it) }
142
+ billing.optString("administrativeArea").takeIf { it.isNotEmpty() }?.let { address.put("state", it) }
143
+ billing.optString("postalCode").takeIf { it.isNotEmpty() }?.let { address.put("postal_code", it) }
144
+ billing.optString("countryCode").takeIf { it.isNotEmpty() }?.let { address.put("country", it.uppercase()) }
145
+ if (address.length() > 0) out.put("address", address)
146
+ return out
147
+ }
148
+
149
+ override fun onNewIntent(intent: Intent) = Unit
150
+
151
+ private fun client(environment: String): PaymentsClient =
152
+ Wallet.getPaymentsClient(
153
+ reactContext,
154
+ Wallet.WalletOptions.Builder()
155
+ .setEnvironment(
156
+ if (environment.equals("PRODUCTION", ignoreCase = true)) {
157
+ WalletConstants.ENVIRONMENT_PRODUCTION
158
+ } else {
159
+ WalletConstants.ENVIRONMENT_TEST
160
+ },
161
+ )
162
+ .build(),
163
+ )
164
+
165
+ private fun baseCardMethod(config: JSONObject): JSONObject =
166
+ JSONObject()
167
+ .put("type", "CARD")
168
+ .put(
169
+ "parameters",
170
+ JSONObject()
171
+ .put("allowedAuthMethods", config.optJSONArray("allowedAuthMethods") ?: defaultAuthMethods())
172
+ .put("allowedCardNetworks", config.optJSONArray("allowedCardNetworks") ?: defaultNetworks()),
173
+ )
174
+
175
+ private fun tokenizedCardMethod(request: JSONObject): JSONObject {
176
+ val method = baseCardMethod(request).put(
177
+ "tokenizationSpecification",
178
+ JSONObject()
179
+ .put("type", "PAYMENT_GATEWAY")
180
+ .put(
181
+ "parameters",
182
+ JSONObject()
183
+ .put("gateway", request.optString("gateway"))
184
+ .put("gatewayMerchantId", request.optString("gatewayMerchantId")),
185
+ ),
186
+ )
187
+ // Mirrors iOS's requiredBillingContactFields: ask only when no address element is mounted.
188
+ // FULL rather than MIN, because the mint wants line1/city/state, not just a postal code.
189
+ if (request.optBoolean("requiresBillingAddress", false)) {
190
+ method.getJSONObject("parameters")
191
+ .put("billingAddressRequired", true)
192
+ .put("billingAddressParameters", JSONObject().put("format", "FULL"))
193
+ }
194
+ return method
195
+ }
196
+
197
+ private fun defaultAuthMethods() = JSONArray().put("PAN_ONLY").put("CRYPTOGRAM_3DS")
198
+
199
+ private fun defaultNetworks() =
200
+ JSONArray().put("VISA").put("MASTERCARD").put("AMEX").put("DISCOVER")
201
+
202
+ companion object {
203
+ const val NAME = "WhopPlatformPay"
204
+ /**
205
+ * Low 16 bits only. FragmentActivity's startActivityForResult rejects a request code with any
206
+ * high bit set, so the "WHOP" spelling (0x57484F50) made AutoResolveHelper throw at the moment
207
+ * the sheet should have opened — the tile rendered and could never complete a payment.
208
+ */
209
+ private const val LOAD_PAYMENT_DATA = 0x4F50
210
+ private val ApiExceptionClass = com.google.android.gms.common.api.ApiException::class.java
211
+ }
212
+ }
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,59 @@
1
+ // swift-tools-version: 6.0
2
+ import PackageDescription
3
+
4
+ /// The two relative paths below are fixed constants, not app-specific: React Native's autolinker
5
+ /// exposes every self-managed dependency through a symlink at
6
+ /// <appRoot>/ios/build/generated/autolinking/libs/<SwiftName>/, a known depth from the app root.
7
+ let reactHeaders: [Target.Dependency] = [
8
+ .product(name: "ReactHeaders", package: "ReactNative"),
9
+ .product(name: "ReactNativeHeaders", package: "ReactNative"),
10
+ .product(name: "ReactNativeDependenciesHeaders", package: "ReactNative"),
11
+ .product(name: "ReactAppHeaders", package: "React-GeneratedCode"),
12
+ ]
13
+
14
+ let package = Package(
15
+ name: "ElementsReactNative",
16
+ // .v15, NOT 15.1, even though React Native declares 15.1 and generates an app target at 15.1:
17
+ // its autolink aggregate target is built at 15.0, and a package product demanding more than
18
+ // that fails to link with "requires minimum platform version 15.1 ... but this target supports
19
+ // 15.0". The floor a CONSUMER meets is still 15.1, set by the app target React Native writes.
20
+ platforms: [.iOS(.v15)],
21
+ products: [
22
+ .library(name: "ElementsReactNative", targets: ["ElementsReactNative", "ElementsReactNativeObjC"]),
23
+ ],
24
+ dependencies: [
25
+ .package(name: "ReactNative", path: "../../../../xcframeworks"),
26
+ .package(name: "React-GeneratedCode", path: "../../../ios"),
27
+ ],
28
+ targets: [
29
+ // Generated by scripts/codegen-shims.mjs. SwiftPM cannot mix languages in one target, and
30
+ // only the objc -> swift direction is supported, so this target must come first.
31
+ .target(
32
+ name: "ElementsReactNativeObjC",
33
+ dependencies: reactHeaders,
34
+ path: "generated",
35
+ publicHeadersPath: ".",
36
+ cSettings: [.headerSearchPath(".")],
37
+ cxxSettings: [
38
+ .headerSearchPath("."),
39
+ .define("DEBUG", .when(configuration: .debug)),
40
+ // React.xcframework has a configuration-gated C++ ABI; without this, Release links
41
+ // against Debug vtables and fails with undefined symbols.
42
+ .define("NDEBUG", .when(configuration: .release)),
43
+ ]
44
+ ),
45
+ .target(
46
+ name: "ElementsReactNative",
47
+ dependencies: reactHeaders + ["ElementsReactNativeObjC"],
48
+ path: "Sources/ElementsReactNative",
49
+ linkerSettings: [
50
+ .linkedFramework("UIKit"),
51
+ .linkedFramework("Foundation"),
52
+ .linkedFramework("PassKit"),
53
+ .linkedFramework("AuthenticationServices"),
54
+ .linkedFramework("MapKit"),
55
+ ]
56
+ ),
57
+ ],
58
+ cxxLanguageStandard: .cxx20
59
+ )
@@ -0,0 +1,189 @@
1
+ import Foundation
2
+ import MapKit
3
+
4
+ /// Street suggestions from MapKit, on-device and with no API key or third-party SDK, where the web
5
+ /// AddressElement asks Google Places.
6
+ ///
7
+ /// This is the Turbo Module port of `MapKitAddressSuggestionProvider` in WhopUI, and it keeps that
8
+ /// implementation's hard-won details rather than reinventing them:
9
+ ///
10
+ /// - Listing and resolving are separate, because that is what MapKit offers.
11
+ /// `MKLocalSearchCompleter` answers a fragment with many candidates; `MKLocalSearch` resolves
12
+ /// ONE place and carries the address components. Using the search for the list gives a
13
+ /// one-row list of frequently the wrong place.
14
+ /// - MapKit does not call the delegate again for an UNCHANGED fragment, so a repeat query must
15
+ /// return what the completer already holds or the caller waits forever.
16
+ /// - Rows are keyed by content, not by position, so a refresh reuses a row rather than tearing it
17
+ /// out from under a finger — and a resolve that lands after a newer query still resolves the
18
+ /// row that was tapped.
19
+ ///
20
+ /// `@unchecked Sendable` with a real invariant: every mutable field is touched only on the main
21
+ /// queue, where MapKit's delegate callbacks also arrive.
22
+ @objc(WhopAddressSuggestionsImpl)
23
+ public final class WhopAddressSuggestionsImpl: NSObject, @unchecked Sendable {
24
+ private let completer = MKLocalSearchCompleter()
25
+ private var pending: (@Sendable (Any?) -> Void)?
26
+ private var completionsByID: [String: MKLocalSearchCompletion] = [:]
27
+ private var completionOrder: [String] = []
28
+ private var search: MKLocalSearch?
29
+
30
+ override public init() {
31
+ super.init()
32
+ DispatchQueue.main.async { [weak self] in
33
+ guard let self else { return }
34
+ self.completer.resultTypes = .address
35
+ self.completer.delegate = self
36
+ }
37
+ }
38
+
39
+ @objc public func isAvailable() -> Bool {
40
+ true
41
+ }
42
+
43
+ @objc public func suggest(
44
+ _ query: String,
45
+ resolve: @escaping @Sendable (Any?) -> Void,
46
+ reject _: @escaping @Sendable (String, String) -> Void
47
+ ) {
48
+ let trimmed = query.trimmingCharacters(in: .whitespacesAndNewlines)
49
+ guard trimmed.count >= Self.minimumQueryLength else {
50
+ resolve(Self.empty)
51
+ return
52
+ }
53
+ DispatchQueue.main.async { [weak self] in
54
+ guard let self else {
55
+ resolve(Self.empty)
56
+ return
57
+ }
58
+ // An unchanged fragment never fires the delegate again, so answer from what is already
59
+ // there. Without this a repeated query hangs until the caller gives up.
60
+ if self.completer.queryFragment == trimmed {
61
+ resolve(self.rows(from: self.completer.results))
62
+ return
63
+ }
64
+ // Settle the superseded query rather than leaving its promise dangling.
65
+ self.pending?(Self.empty)
66
+ self.pending = resolve
67
+ self.completer.queryFragment = trimmed
68
+ }
69
+ }
70
+
71
+ @objc public func details(
72
+ _ suggestionId: String,
73
+ resolve: @escaping @Sendable (Any?) -> Void,
74
+ reject _: @escaping @Sendable (String, String) -> Void
75
+ ) {
76
+ DispatchQueue.main.async { [weak self] in
77
+ guard let self, let completion = self.completionsByID[suggestionId] else {
78
+ resolve(Self.emptyObject)
79
+ return
80
+ }
81
+ self.search?.cancel()
82
+ let search = MKLocalSearch(request: MKLocalSearch.Request(completion: completion))
83
+ self.search = search
84
+ search.start { response, _ in
85
+ guard let item = response?.mapItems.first else {
86
+ resolve(Self.emptyObject)
87
+ return
88
+ }
89
+ resolve(Self.components(of: item, subtitle: completion.subtitle))
90
+ }
91
+ }
92
+ }
93
+
94
+ private func rows(from completions: [MKLocalSearchCompletion]) -> String {
95
+ var rows: [[String: String]] = []
96
+ for completion in completions.prefix(Self.resultLimit) {
97
+ let id = "\(completion.title)\n\(completion.subtitle)"
98
+ if completionsByID.updateValue(completion, forKey: id) == nil {
99
+ completionOrder.append(id)
100
+ } else if let seen = completionOrder.firstIndex(of: id) {
101
+ // Insertion order alone evicts from the front, and a REPEAT hit left its id
102
+ // there — so a row still on screen could be trimmed, and its later tap would
103
+ // resolve to nothing.
104
+ completionOrder.remove(at: seen)
105
+ completionOrder.append(id)
106
+ }
107
+ let label = completion.subtitle.isEmpty
108
+ ? completion.title
109
+ : "\(completion.title), \(completion.subtitle)"
110
+ rows.append(["id": id, "label": label])
111
+ }
112
+ trimCompletions()
113
+ guard let data = try? JSONSerialization.data(withJSONObject: rows),
114
+ let json = String(data: data, encoding: .utf8)
115
+ else { return Self.empty }
116
+ return json
117
+ }
118
+
119
+ /// Bounded, so a long typing session does not grow the map without limit — but generous enough
120
+ /// that a resolve arriving after several newer queries still finds its row.
121
+ private func trimCompletions() {
122
+ let overflow = max(0, completionOrder.count - Self.cacheLimit)
123
+ guard overflow > 0 else { return }
124
+ for id in completionOrder.prefix(overflow) {
125
+ completionsByID[id] = nil
126
+ }
127
+ completionOrder.removeFirst(overflow)
128
+ }
129
+
130
+ /// MKMapItem.placemark is the only iOS 18 API that hands back address COMPONENTS; the iOS 26
131
+ /// replacements (MKAddress, MKAddressRepresentations) return formatted strings, which a form
132
+ /// that fills city / subdivision / postal-code fields cannot use.
133
+ private static func components(of item: MKMapItem, subtitle: String) -> String {
134
+ let placemark = item.placemark
135
+ var row: [String: String] = [:]
136
+ row["streetNumber"] = placemark.subThoroughfare ?? ""
137
+ row["street"] = placemark.thoroughfare ?? ""
138
+ // A row with no street components at all still names something usable.
139
+ if (placemark.subThoroughfare ?? "").isEmpty, (placemark.thoroughfare ?? "").isEmpty {
140
+ row["line1"] = item.name ?? ""
141
+ }
142
+ row["city"] = city(from: placemark, subtitle: subtitle) ?? ""
143
+ row["state"] = placemark.administrativeArea ?? ""
144
+ row["postalCode"] = placemark.postalCode ?? ""
145
+ row["country"] = placemark.isoCountryCode?.uppercased() ?? ""
146
+ guard let data = try? JSONSerialization.data(withJSONObject: row),
147
+ let json = String(data: data, encoding: .utf8)
148
+ else { return emptyObject }
149
+ return json
150
+ }
151
+
152
+ /// NYC boroughs all resolve to locality "New York"; a row that never names it names the real city.
153
+ private static func city(from placemark: MKPlacemark, subtitle: String) -> String? {
154
+ let components = subtitle
155
+ .components(separatedBy: ",")
156
+ .map { $0.trimmingCharacters(in: .whitespaces) }
157
+ let names = { (name: String) in
158
+ components.contains { $0 == name || $0.hasPrefix("\(name) ") }
159
+ }
160
+ if let locality = placemark.locality, names(locality) { return locality }
161
+ guard let state = placemark.administrativeArea,
162
+ let stateIndex = components.firstIndex(where: { $0 == state || $0.hasPrefix("\(state) ") }),
163
+ stateIndex > 0,
164
+ !components[stateIndex - 1].isEmpty
165
+ else { return placemark.locality }
166
+ return components[stateIndex - 1]
167
+ }
168
+
169
+ private static let minimumQueryLength = 3
170
+ private static let resultLimit = 5
171
+ private static let cacheLimit = 100
172
+ private static let empty = "[]"
173
+ private static let emptyObject = "{}"
174
+ }
175
+
176
+ extension WhopAddressSuggestionsImpl: MKLocalSearchCompleterDelegate {
177
+ public func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
178
+ let settle = pending
179
+ pending = nil
180
+ settle?(rows(from: completer.results))
181
+ }
182
+
183
+ public func completer(_: MKLocalSearchCompleter, didFailWithError _: Error) {
184
+ // A failed lookup leaves the buyer typing the address by hand, which is where they started.
185
+ let settle = pending
186
+ pending = nil
187
+ settle?(Self.empty)
188
+ }
189
+ }