@gbitx/pay-react-native 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — 2026-07-12
4
+
5
+ Initial React Native SDK for GbitXPay.
6
+
7
+ - `GbitXPay.configure()` — validates the publishable key against `GET /v1/sdk/config` (typed error taxonomy, environment cross‑check, bounded timeout).
8
+ - `GbitXPay.present()` — opens the hosted checkout in a hardened, origin‑locked WebView and resolves a single typed `PaymentResult`.
9
+ - `<GbitXPayProvider>` host + `useGbitXPay()` hook.
10
+ - Security hardening per the mobile‑SDK design: pinned checkout origin, navigation lock, committed‑origin message trust gate, defensive message parsing + payment whitelist, single settle latch, credential redaction, file/window/cookie lockdown, expiry backstop, no native module.
11
+ - Full unit test suite over the pure core (validation, config taxonomy, message parsing, errors, redaction, controller).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GBIT TECHNOLOGIES LIMITED COMPANY
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,247 @@
1
+ # @gbitx/pay-react-native
2
+
3
+ Accept crypto payments in your React Native app with **GbitXPay**. The SDK opens the hosted GbitX checkout inside a hardened, origin‑locked WebView and hands you a single typed result.
4
+
5
+ - One‑line `present()` per payment; `configure()` once at startup.
6
+ - No secret key, no address handling, no wallet code ever ships in your app.
7
+ - Pure JS over `react-native-webview` — no native module of its own. Works on the New Architecture.
8
+ - TypeScript‑first. Fully typed public surface.
9
+
10
+ > **Read this first.** Fulfilment is driven **only** by the signed server webhook (or a server‑side read of the payment with your secret key). The SDK's `confirmed` result is a **UX signal**, not proof of payment — it can be forged on a compromised device. Never release goods or credit an account from an SDK callback.
11
+
12
+ ---
13
+
14
+ ## How it works
15
+
16
+ ```
17
+ Your server (SECRET key gk_) Your app (PUBLISHABLE key pk_)
18
+ ──────────────────────────── ─────────────────────────────
19
+ POST /v1/payments ──────────────▶ { id, clientToken }
20
+ │ │
21
+ └── hands { id, clientToken } to app ──────┘
22
+
23
+ GbitXPay.present({ paymentId, clientToken })
24
+
25
+ opens pay.gbitx.com checkout in a WebView
26
+
27
+ signed webhook ◀──── PAYMENT_CONFIRMED ───┤ (source of truth)
28
+ fulfil the order here └── result: { status: 'confirmed', … } (UX only)
29
+ ```
30
+
31
+ Your **server** creates the payment with your **secret** key and gives the app only `{ paymentId, clientToken }`. The app never calls `POST /v1/payments`, and the secret key must **never** be in your app binary.
32
+
33
+ The SDK validates your publishable key against the API host `gateway.gbitx.com` (the `apiBaseUrl` default), which returns the separate, pinned checkout origin `pay.gbitx.com` that the WebView locks to. They are intentionally different hosts. (`doc.gbitx.com` is the docs site.)
34
+
35
+ ---
36
+
37
+ ## Install
38
+
39
+ > **Pre‑publish:** `@gbitx/pay-react-native` is not on npm yet, so the command below 404s today. Until it publishes, resolve it locally — `npm pack` in the SDK repo and install the tarball (`npm install ../path/gbitx-pay-react-native-0.1.0.tgz`), or a `file:` path. `react-native-webview` is always a real peer dependency you install.
40
+
41
+ ```sh
42
+ npm install @gbitx/pay-react-native react-native-webview
43
+ # or: yarn add @gbitx/pay-react-native react-native-webview
44
+ cd ios && pod install # bare RN only
45
+ ```
46
+
47
+ `react`, `react-native`, and `react-native-webview` are **peer dependencies** you provide (`react-native-webview` **>=13 and <15**). Minimum: iOS 13 / Android 6 (API 23) with an up‑to‑date System WebView.
48
+
49
+ ---
50
+
51
+ ## Quickstart
52
+
53
+ **1. Mount the provider once at your app root:**
54
+
55
+ ```tsx
56
+ import { GbitXPayProvider } from '@gbitx/pay-react-native';
57
+
58
+ export default function App() {
59
+ return (
60
+ <GbitXPayProvider>
61
+ <RootNavigator />
62
+ </GbitXPayProvider>
63
+ );
64
+ }
65
+ ```
66
+
67
+ **2. Configure once (e.g. on startup), with your publishable key:**
68
+
69
+ ```ts
70
+ import { GbitXPay } from '@gbitx/pay-react-native';
71
+
72
+ await GbitXPay.configure({ publishableKey: 'pk_live_…' });
73
+ ```
74
+
75
+ **3. Present a checkout when the user pays:**
76
+
77
+ ```ts
78
+ // paymentId + clientToken come from YOUR server's POST /v1/payments response.
79
+ const result = await GbitXPay.present({ paymentId, clientToken });
80
+
81
+ switch (result.status) {
82
+ case 'confirmed': showThankYou(result.payment); break; // UX only — fulfil via webhook
83
+ case 'cancelled': /* user closed */ break;
84
+ case 'expired': /* 60‑min window elapsed */ break;
85
+ case 'failed': /* payment failed */ break;
86
+ }
87
+ ```
88
+
89
+ Prefer hooks?
90
+
91
+ ```tsx
92
+ import { useGbitXPay } from '@gbitx/pay-react-native';
93
+ const { present } = useGbitXPay();
94
+ ```
95
+
96
+ ---
97
+
98
+ ## Your server: creating the payment
99
+
100
+ The app must receive `{ paymentId, clientToken }` from your backend. Your backend calls `POST /v1/payments` with the **secret** key:
101
+
102
+ ```js
103
+ // Node/Express — on YOUR server. Never in the app.
104
+ app.post('/checkout', async (req, res) => {
105
+ const r = await fetch('https://gateway.gbitx.com/v1/payments', {
106
+ method: 'POST',
107
+ headers: {
108
+ Authorization: `Bearer ${process.env.GBITX_SECRET_KEY}`, // gk_live_…
109
+ 'Content-Type': 'application/json',
110
+ },
111
+ body: JSON.stringify({ amount: 49.99, currency: 'USD', description: 'Order #1234' }),
112
+ });
113
+ const { payment } = await r.json();
114
+ // Send ONLY these two to the app:
115
+ res.json({ paymentId: payment.id, clientToken: payment.clientToken });
116
+ });
117
+ ```
118
+
119
+ Then handle the signed webhook (`PAYMENT_CONFIRMED`) to fulfil the order. See the [example server](./example/server/create-payment.js).
120
+
121
+ ---
122
+
123
+ ## Security model
124
+
125
+ Three credentials, so nothing dangerous ever ships in a binary:
126
+
127
+ | Credential | Lives | Can do |
128
+ | --- | --- | --- |
129
+ | **Secret key** `gk_` | Your **server** | Create/read all payments |
130
+ | **Publishable key** `pk_` | Your **app** (this SDK) | Identify your account. **No** payment authority |
131
+ | **Client token** | Your app, per payment | Drive **one** 60‑minute checkout |
132
+
133
+ The SDK additionally:
134
+
135
+ - **Pins the checkout origin** from your validated config and locks the WebView to it (`originWhitelist`, navigation lock, no child windows, no file access, `mixedContentMode: never`).
136
+ - **Gates every bridge message** on the WebView's committed origin and re‑validates its shape; unknown/oversized/malformed messages are dropped.
137
+ - **Never logs** your key or the client token, and never surfaces the checkout URL (only its origin + path, redacted, when debug is on). The client token rides in the URL *fragment* (`#t=…`), so the **SDK itself never transmits it** and holds it in memory only, clearing it when the checkout closes. (The pinned checkout page forwards it over TLS to its own origin's gateway to poll status / authorize cancel — inside the pinned‑origin trust boundary.)
138
+ - Settles each checkout **exactly once** and never treats a `confirmed` event as fulfilment.
139
+
140
+ ### Must‑know rules
141
+
142
+ - **Fulfil on your server, from the signed webhook** (`PAYMENT_CONFIRMED`) or a server‑side `GET /v1/payments/:id` with your secret key. SDK callbacks are UX only and forgeable on a compromised device.
143
+ - **The app holds a publishable key only.** Passing a `gk_` secret key throws `secret_key_used` — remove it from your app immediately.
144
+ - **The client token is single‑payment and short‑lived** (dies with the 60‑minute payment). The SDK never persists it and it must never be logged. A new attempt requires your server to mint a new payment.
145
+ - **Networking is HTTPS‑only to pay.gbitx.com.** Do **not** add an iOS ATS exception (`NSAllowsArbitraryLoads`) or Android cleartext (`usesCleartextTraffic="true"`) on the SDK's behalf — none is needed and it weakens your whole app.
146
+
147
+ ---
148
+
149
+ ## App Store & Play Store
150
+
151
+ Crypto is permitted for **physical goods** and **real‑world services** (Apple Guideline 3.1.1 / Google Play Billing policy). **Digital goods or services consumed in‑app must use Apple IAP / Google Play Billing** — do not use this SDK for those. Crypto‑touching apps should be published under an established legal entity.
152
+
153
+ ---
154
+
155
+ ## API
156
+
157
+ ### `GbitXPay.configure(options): Promise<void>`
158
+
159
+ | Option | Type | Default | Notes |
160
+ | --- | --- | --- | --- |
161
+ | `publishableKey` | `string` | — | `pk_live_…` / `pk_test_…`. Validated against the server. |
162
+ | `environment` | `'live' \| 'test'` | inferred | If set, must match the key and the server. |
163
+ | `timeoutMs` | `number` | `10000` | Bounded timeout for the validation call. |
164
+ | `apiBaseUrl` | `string` | `https://gateway.gbitx.com` | Override for staging. Must be https. |
165
+ | `debug` | `boolean` | `false` | Redacted debug logging (never prints key/token). |
166
+
167
+ Must complete before `present()`. Rejects with a typed `GbitXPayError` (see below).
168
+
169
+ ### `GbitXPay.present(options): Promise<PaymentResult>`
170
+
171
+ | Option | Type | Notes |
172
+ | --- | --- | --- |
173
+ | `paymentId` | `string` | From your server's create response. |
174
+ | `clientToken` | `string` | From your server's create response. |
175
+ | `onLoaded?` | `() => void` | Fires once when the checkout is interactive. |
176
+ | `onEvent?` | `(e: CheckoutEvent) => void` | Streams lifecycle events (UX only). |
177
+
178
+ Resolves with a `PaymentResult`:
179
+
180
+ ```ts
181
+ type PaymentResult =
182
+ | { status: 'confirmed'; payment: SanitizedPayment | null }
183
+ | { status: 'cancelled'; reason?: 'user_closed' | 'dismissed' | 'back_button' | 'load_failed'; payment?: SanitizedPayment | null }
184
+ | { status: 'expired'; reason?: 'expired' | 'client_backstop'; payment?: SanitizedPayment | null }
185
+ | { status: 'failed'; payment?: SanitizedPayment | null };
186
+ ```
187
+
188
+ A cancelled/expired/failed outcome is a **normal result**, not an error. It **rejects** only for SDK/credential faults.
189
+
190
+ ### Errors — `GbitXPayError`
191
+
192
+ `error.code` is one of:
193
+
194
+ | code | meaning |
195
+ | --- | --- |
196
+ | `not_configured` | `present()` before a successful `configure()` |
197
+ | `not_mounted` | no `<GbitXPayProvider>` in the tree |
198
+ | `invalid_key` | key wrong/revoked/malformed |
199
+ | `secret_key_used` | a `gk_` secret key was passed — remove it from the app |
200
+ | `environment_mismatch` | key env and server env disagree |
201
+ | `onboarding_required` | merchant not yet approved for this live key (`meta.onboardingStatus`) |
202
+ | `merchant_suspended` | merchant account inactive |
203
+ | `rate_limited` | too many config calls (`meta.retryAfterSec`) |
204
+ | `network_error` | couldn't reach GbitX (offline/DNS/TLS/timeout) |
205
+ | `server_error` | GbitX returned 5xx or an unusable response |
206
+ | `invalid_arguments` | bad `paymentId`/`clientToken`, or a checkout already open |
207
+
208
+ ```ts
209
+ import { GbitXPayError, isGbitXPayError } from '@gbitx/pay-react-native';
210
+ try {
211
+ await GbitXPay.configure({ publishableKey });
212
+ } catch (e) {
213
+ if (isGbitXPayError(e) && e.code === 'secret_key_used') { /* fix your integration */ }
214
+ }
215
+ ```
216
+
217
+ ### `SanitizedPayment`
218
+
219
+ Exactly the fields the checkout emits — `address`, `txHash`, and `externalRef` are intentionally **not** included (never readable client‑side). Amounts are decimal‑safe strings.
220
+
221
+ ```ts
222
+ interface SanitizedPayment {
223
+ id: string;
224
+ status: 'PENDING' | 'AWAITING_PAYMENT' | 'UNDERPAID' | 'CONFIRMING' | 'CONFIRMED' | 'EXPIRED' | 'FAILED' | 'CANCELLED';
225
+ amount: string; currency: string;
226
+ crypto: string | null; network: string | null;
227
+ amountCrypto: string | null; amountPaid: string | null;
228
+ mode: 'LIVE' | 'SANDBOX';
229
+ confirmedAt: string | null; expiresAt: string;
230
+ }
231
+ ```
232
+
233
+ ---
234
+
235
+ ## Requirements
236
+
237
+ | | Version |
238
+ | --- | --- |
239
+ | react | ≥ 17 |
240
+ | react-native | ≥ 0.71 (New Architecture supported) |
241
+ | react-native-webview | ≥ 13 < 15 |
242
+ | iOS | 13+ |
243
+ | Android | 6 / API 23+ |
244
+
245
+ ## License
246
+
247
+ MIT © GBIT TECHNOLOGIES LIMITED COMPANY
package/SECURITY.md ADDED
@@ -0,0 +1,28 @@
1
+ # Security
2
+
3
+ This SDK opens the GbitXPay hosted checkout inside a hardened `react-native-webview`. It was built against a written hardening spec and passed an adversarial review. This document states the trust model, what the SDK guarantees, and the residual risks it cannot close on its own.
4
+
5
+ ## The one invariant
6
+
7
+ **Fulfilment is driven only by the signed server webhook** (`PAYMENT_CONFIRMED`) or a server‑side `GET /v1/payments/:id` authenticated with your **secret** key. The SDK's `confirmed` result is a **UX signal**, not proof of payment. Everything below assumes you honor this — it is what bounds the blast radius of every residual risk.
8
+
9
+ ## What the SDK enforces
10
+
11
+ - **Pinned origin.** The checkout origin is taken from your validated `GET /v1/sdk/config`, asserted `https`, and frozen. `present()` cannot override it.
12
+ - **Sole‑gate navigation lock.** `originWhitelist` is `['*']` so the library never auto‑decides (its built‑in whitelist is a loose *prefix* match); every navigation is routed to an exact‑origin check. Only the pinned origin over `https` may load. Off‑origin navigations are blocked and opened **nowhere** — the SDK never calls `Linking.openURL`, so a compromised page cannot force‑launch an external URL or wallet deep‑link.
13
+ - **Authenticated message trust gate.** Bridge messages are trusted on the WebView's **authenticated sender origin** (`event.nativeEvent.url` — iOS `WKScriptMessage.frameInfo`, Android `WebMessageListener` sourceOrigin), with exact origin + `/p/` path equality, falling back to the committed top‑frame only when the platform provides no sender URL. Messages are **bound to the active `paymentId`**.
14
+ - **Defensive parsing.** Bridge payloads are size‑capped, `JSON.parse`d in a guard, shape‑validated against a known event set, and re‑picked into a fresh object holding only the whitelisted payment fields (no `address`, `txHash`, `externalRef`; no prototype‑pollution or function keys survive).
15
+ - **Single settle.** Each checkout resolves exactly once; a native dismissal (back, swipe, unmount) still settles so `present()` never hangs, and a fixed 65‑minute client backstop (just beyond the 60‑minute server payment expiry) bounds the session.
16
+ - **Credential hygiene.** The publishable key, client token, and full checkout URL never reach a log. The token rides in the URL *fragment* (`#t=…`), so **the SDK itself never transmits it in a request** and holds it in memory only, clearing it when the checkout closes. (The pinned checkout page's own JS does read the fragment and forward the token over TLS to its own origin's gateway to poll status and authorize cancel — that is inside the pinned‑origin trust boundary, not a leak to a third party.) `GbitXPayError` keeps its transport `cause` non‑enumerable.
17
+ - **WebView lockdown.** No file access, no multiple windows, no third‑party/shared cookies, `mixedContentMode: 'never'`, no injected JS, no camera/mic grants.
18
+
19
+ ## Residual risks (cannot be closed client‑side)
20
+
21
+ 1. **A compromised checkout origin can emit a forged `confirmed`.** If `pay.gbitx.com` itself is compromised or MITM'd, a same‑origin page can send any bridge event. No client can distinguish this. **Mitigation: webhook‑is‑truth** — never fulfil from the SDK result. TLS + the origin lock prevent MITM; this is why fulfilment lives on your server.
22
+ 2. **Android sub‑frame navigations aren't gated by `onShouldStartLoadWithRequest`** (a platform limitation). The checkout page ships no iframes and `mixedContentMode: 'never'` blocks `http` frames; any off‑origin frame that did load could not forge a trusted message (the authenticated‑origin gate rejects it) nor navigate the top frame. Defense in depth belongs in the page's CSP (`frame-src`).
23
+ 3. **iOS App‑Bound Domains** (`WKAppBoundDomains` / `limitsNavigationsToAppBoundDomains`) is an optional native origin lock we do **not** force, because it requires merchant `Info.plist` entries and constrains other WebViews in the app. Consider adding it if your app uses only a small set of WebView domains.
24
+ 4. **TLS public‑key pinning** for `pay.gbitx.com` is a recommended fast‑follow, not enabled by default.
25
+
26
+ ## Reporting a vulnerability
27
+
28
+ Email **security@gbitx.com**. Please do not open a public issue for security reports.