@ozura/elements 0.1.0-beta.7 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +906 -663
- package/dist/frame/element-frame.js +77 -57
- package/dist/frame/element-frame.js.map +1 -1
- package/dist/frame/tokenizer-frame.html +1 -1
- package/dist/frame/tokenizer-frame.js +211 -94
- package/dist/frame/tokenizer-frame.js.map +1 -1
- package/dist/oz-elements.esm.js +817 -230
- package/dist/oz-elements.esm.js.map +1 -1
- package/dist/oz-elements.umd.js +817 -229
- package/dist/oz-elements.umd.js.map +1 -1
- package/dist/react/frame/tokenizerFrame.d.ts +32 -0
- package/dist/react/index.cjs.js +968 -218
- package/dist/react/index.cjs.js.map +1 -1
- package/dist/react/index.esm.js +965 -219
- package/dist/react/index.esm.js.map +1 -1
- package/dist/react/react/index.d.ts +148 -6
- package/dist/react/sdk/OzElement.d.ts +34 -3
- package/dist/react/sdk/OzVault.d.ts +68 -4
- package/dist/react/sdk/errors.d.ts +9 -0
- package/dist/react/sdk/index.d.ts +29 -0
- package/dist/react/server/index.d.ts +181 -17
- package/dist/react/types/index.d.ts +69 -19
- package/dist/react/utils/appearance.d.ts +9 -0
- package/dist/react/utils/cardUtils.d.ts +14 -0
- package/dist/react/utils/uuid.d.ts +12 -0
- package/dist/server/frame/tokenizerFrame.d.ts +32 -0
- package/dist/server/index.cjs.js +608 -71
- package/dist/server/index.cjs.js.map +1 -1
- package/dist/server/index.esm.js +606 -72
- package/dist/server/index.esm.js.map +1 -1
- package/dist/server/sdk/OzElement.d.ts +34 -3
- package/dist/server/sdk/OzVault.d.ts +68 -4
- package/dist/server/sdk/errors.d.ts +9 -0
- package/dist/server/sdk/index.d.ts +29 -0
- package/dist/server/server/index.d.ts +181 -17
- package/dist/server/types/index.d.ts +69 -19
- package/dist/server/utils/appearance.d.ts +9 -0
- package/dist/server/utils/cardUtils.d.ts +14 -0
- package/dist/server/utils/uuid.d.ts +12 -0
- package/dist/types/frame/tokenizerFrame.d.ts +32 -0
- package/dist/types/sdk/OzElement.d.ts +34 -3
- package/dist/types/sdk/OzVault.d.ts +68 -4
- package/dist/types/sdk/errors.d.ts +9 -0
- package/dist/types/sdk/index.d.ts +29 -0
- package/dist/types/server/index.d.ts +181 -17
- package/dist/types/types/index.d.ts +69 -19
- package/dist/types/utils/appearance.d.ts +9 -0
- package/dist/types/utils/cardUtils.d.ts +14 -0
- package/dist/types/utils/uuid.d.ts +12 -0
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -1,53 +1,79 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @ozura/elements
|
|
2
2
|
|
|
3
|
-
PCI-
|
|
3
|
+
PCI-isolated card and bank account tokenization SDK for the Ozura Vault.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Card data is collected inside Ozura-hosted iframes so raw numbers never touch your JavaScript bundle, your server logs, or your network traffic. The tokenizer communicates directly with the vault using `MessageChannel` port transfers — the merchant page acts as a layout host only.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Table of contents
|
|
10
|
+
|
|
11
|
+
- [How it works](#how-it-works)
|
|
12
|
+
- [Installation](#installation)
|
|
13
|
+
- [Quick start — React](#quick-start--react)
|
|
14
|
+
- [Quick start — Vanilla JS](#quick-start--vanilla-js)
|
|
15
|
+
- [Server setup](#server-setup)
|
|
16
|
+
- [Wax key endpoint](#wax-key-endpoint)
|
|
17
|
+
- [Card sale endpoint](#card-sale-endpoint)
|
|
18
|
+
- [Vanilla JS API](#vanilla-js-api)
|
|
19
|
+
- [OzVault.create()](#ozvaultcreate)
|
|
20
|
+
- [vault.createElement()](#vaultcreateelementtypeoptions)
|
|
21
|
+
- [vault.createToken()](#vaultcreatetokenoptions)
|
|
22
|
+
- [vault.createBankElement()](#vaultcreatebankelement)
|
|
23
|
+
- [vault.createBankToken()](#vaultcreatebanktokenoptions)
|
|
24
|
+
- [vault.destroy()](#vaultdestroy)
|
|
25
|
+
- [OzElement events](#ozelement-events)
|
|
26
|
+
- [React API](#react-api)
|
|
27
|
+
- [OzElements provider](#ozelements-provider)
|
|
28
|
+
- [OzCard](#ozcard)
|
|
29
|
+
- [Individual field components](#individual-field-components)
|
|
30
|
+
- [OzBankCard](#ozbankcard)
|
|
31
|
+
- [useOzElements()](#useozelements)
|
|
32
|
+
- [Styling](#styling)
|
|
33
|
+
- [Per-element styles](#per-element-styles)
|
|
34
|
+
- [Global appearance](#global-appearance)
|
|
35
|
+
- [Custom fonts](#custom-fonts)
|
|
36
|
+
- [Billing details](#billing-details)
|
|
37
|
+
- [Error handling](#error-handling)
|
|
38
|
+
- [Server utilities](#server-utilities)
|
|
39
|
+
- [Ozura class](#ozura-class)
|
|
40
|
+
- [Route handler factories](#route-handler-factories)
|
|
41
|
+
- [Local development](#local-development)
|
|
42
|
+
- [Content Security Policy](#content-security-policy)
|
|
43
|
+
- [TypeScript reference](#typescript-reference)
|
|
8
44
|
|
|
9
45
|
---
|
|
10
46
|
|
|
11
|
-
##
|
|
47
|
+
## How it works
|
|
12
48
|
|
|
13
|
-
|
|
49
|
+
```
|
|
50
|
+
Merchant page
|
|
51
|
+
├── OzVault (manages tokenizer iframe + element iframes)
|
|
52
|
+
├── [hidden] tokenizer-frame.html ← Ozura origin
|
|
53
|
+
├── [visible] element-frame.html ← card number ─┐ MessageChannel
|
|
54
|
+
├── [visible] element-frame.html ← expiry ├─ port transfer
|
|
55
|
+
└── [visible] element-frame.html ← CVV ─┘
|
|
56
|
+
```
|
|
14
57
|
|
|
15
|
-
|
|
58
|
+
1. `OzVault.create()` mounts a hidden tokenizer iframe and fetches a short-lived **wax key** from your server.
|
|
59
|
+
2. Calling `vault.createElement()` mounts a visible input iframe for each field.
|
|
60
|
+
3. `vault.createToken()` opens a direct `MessageChannel` between each element iframe and the tokenizer iframe. Raw values travel over those ports — they never pass through your JavaScript.
|
|
61
|
+
4. The tokenizer POSTs directly to the vault API over HTTPS and returns a token to your page.
|
|
16
62
|
|
|
17
|
-
|
|
63
|
+
Your server only ever sees a token, never card data.
|
|
18
64
|
|
|
19
65
|
---
|
|
20
66
|
|
|
21
|
-
##
|
|
67
|
+
## Credentials
|
|
22
68
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
│ cardNumber.mount('#card-number');
|
|
32
|
-
│ cardNumber.on('change', ({ valid, cardBrand }) => { ... });
|
|
33
|
-
│
|
|
34
|
-
│ const { token, cvcSession } = await vault.createToken();
|
|
35
|
-
│ // token is yours — use it with any system
|
|
36
|
-
│
|
|
37
|
-
├── <iframe src="https://elements.ozura.com/frame/element-frame.html">
|
|
38
|
-
│ <input type="tel" /> ← your JS cannot access this
|
|
39
|
-
│ </iframe>
|
|
40
|
-
│
|
|
41
|
-
└── <iframe style="display:none" src="https://elements.ozura.com/frame/tokenizer-frame.html">
|
|
42
|
-
Receives raw values from element iframes (same-origin).
|
|
43
|
-
POSTs directly to vault /tokenize.
|
|
44
|
-
Returns token + cvcSession to the SDK.
|
|
45
|
-
</iframe>
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
Raw card data travels one path: element iframe → tokenizer iframe (direct same-origin `postMessage`). Your SDK only ever sees metadata (valid/complete/cardBrand) and the final token.
|
|
49
|
-
|
|
50
|
-
The tokenizer POSTs to the vault with **`X-Wax-Key`** (minted on your backend). The vault secret must **never** appear in the browser or in tokenize requests.
|
|
69
|
+
| Credential | Format | Where it lives | Required for |
|
|
70
|
+
|---|---|---|---|
|
|
71
|
+
| **Vault pub key** | `pk_live_…` or `pk_prod_…` | Frontend env var (safe to expose) | All integrations |
|
|
72
|
+
| **Vault API key** | `key_…` | Server env var — **never in the browser** | Minting wax keys (all integrations) |
|
|
73
|
+
| **Pay API key** | `ak_…` | Server env var only | OzuraPay merchants (card charging) |
|
|
74
|
+
| **Merchant ID** | `ozu_…` | Server env var only | OzuraPay merchants (card charging) |
|
|
75
|
+
|
|
76
|
+
If you are not routing payments through OzuraPay you only need the vault pub key (frontend) and vault API key (backend).
|
|
51
77
|
|
|
52
78
|
---
|
|
53
79
|
|
|
@@ -57,831 +83,1048 @@ The tokenizer POSTs to the vault with **`X-Wax-Key`** (minted on your backend).
|
|
|
57
83
|
npm install @ozura/elements
|
|
58
84
|
```
|
|
59
85
|
|
|
60
|
-
|
|
86
|
+
React and React DOM are peer dependencies (optional — only needed for `@ozura/elements/react`):
|
|
61
87
|
|
|
62
|
-
```
|
|
63
|
-
|
|
88
|
+
```bash
|
|
89
|
+
npm install react react-dom # if not already installed
|
|
64
90
|
```
|
|
65
91
|
|
|
66
|
-
|
|
92
|
+
**Requirements:** Node ≥ 18, React ≥ 17 (React peer).
|
|
67
93
|
|
|
68
94
|
---
|
|
69
95
|
|
|
70
|
-
## Quick start
|
|
96
|
+
## Quick start — React
|
|
71
97
|
|
|
72
|
-
```
|
|
73
|
-
|
|
98
|
+
```tsx
|
|
99
|
+
// 1. Wrap your checkout in <OzElements>
|
|
100
|
+
import { OzElements, OzCard, useOzElements, createFetchWaxKey } from '@ozura/elements/react';
|
|
74
101
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
102
|
+
function CheckoutPage() {
|
|
103
|
+
return (
|
|
104
|
+
<OzElements
|
|
105
|
+
pubKey="pk_live_..."
|
|
106
|
+
fetchWaxKey={createFetchWaxKey('/api/mint-wax')}
|
|
107
|
+
>
|
|
108
|
+
<CheckoutForm />
|
|
109
|
+
</OzElements>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 2. Collect card data and tokenize
|
|
114
|
+
function CheckoutForm() {
|
|
115
|
+
const { createToken, ready } = useOzElements();
|
|
116
|
+
|
|
117
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
118
|
+
e.preventDefault();
|
|
119
|
+
const { token, cvcSession, billing } = await createToken({
|
|
120
|
+
billing: {
|
|
121
|
+
firstName: 'Jane',
|
|
122
|
+
lastName: 'Smith',
|
|
123
|
+
email: 'jane@example.com',
|
|
124
|
+
address: { line1: '123 Main St', city: 'Austin', state: 'TX', zip: '78701', country: 'US' },
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Send token to your server
|
|
129
|
+
await fetch('/api/charge', {
|
|
80
130
|
method: 'POST',
|
|
81
131
|
headers: { 'Content-Type': 'application/json' },
|
|
82
|
-
body: JSON.stringify({
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
},
|
|
86
|
-
});
|
|
132
|
+
body: JSON.stringify({ token, cvcSession, billing }),
|
|
133
|
+
});
|
|
134
|
+
};
|
|
87
135
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
136
|
+
return (
|
|
137
|
+
<form onSubmit={handleSubmit}>
|
|
138
|
+
<OzCard onChange={(state) => console.log(state.cardBrand)} />
|
|
139
|
+
<button type="submit" disabled={!ready}>Pay</button>
|
|
140
|
+
</form>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
```
|
|
95
144
|
|
|
96
|
-
|
|
97
|
-
const cvv = vault.createElement('cvv');
|
|
145
|
+
---
|
|
98
146
|
|
|
99
|
-
|
|
100
|
-
expiry.mount('#expiry');
|
|
101
|
-
cvv.mount('#cvv');
|
|
147
|
+
## Quick start — Vanilla JS
|
|
102
148
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
});
|
|
149
|
+
```ts
|
|
150
|
+
import { OzVault, createFetchWaxKey } from '@ozura/elements';
|
|
106
151
|
|
|
107
|
-
|
|
108
|
-
|
|
152
|
+
// Declare state BEFORE OzVault.create(). The onReady callback fires when the
|
|
153
|
+
// tokenizer iframe loads — this can happen before create() resolves because the
|
|
154
|
+
// iframe loads concurrently with fetchWaxKey. At that moment, `vault` is still
|
|
155
|
+
// undefined. Do not reference `vault` inside onReady.
|
|
156
|
+
let readyCount = 0;
|
|
157
|
+
let tokenizerIsReady = false;
|
|
109
158
|
|
|
110
|
-
|
|
111
|
-
|
|
159
|
+
function checkReady() {
|
|
160
|
+
if (readyCount === 3 && tokenizerIsReady) enablePayButton();
|
|
161
|
+
}
|
|
112
162
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
await fetch('/api/tokenized', {
|
|
118
|
-
method: 'POST',
|
|
119
|
-
headers: { 'Content-Type': 'application/json' },
|
|
120
|
-
body: JSON.stringify({ token, cvcSession }),
|
|
121
|
-
});
|
|
163
|
+
const vault = await OzVault.create({
|
|
164
|
+
pubKey: 'pk_live_...',
|
|
165
|
+
fetchWaxKey: createFetchWaxKey('/api/mint-wax'),
|
|
166
|
+
onReady: () => { tokenizerIsReady = true; checkReady(); }, // tokenizer iframe loaded
|
|
122
167
|
});
|
|
123
|
-
```
|
|
124
168
|
|
|
125
|
-
|
|
169
|
+
const cardNumberEl = vault.createElement('cardNumber');
|
|
170
|
+
const expiryEl = vault.createElement('expirationDate');
|
|
171
|
+
const cvvEl = vault.createElement('cvv');
|
|
126
172
|
|
|
127
|
-
|
|
173
|
+
cardNumberEl.mount('#card-number');
|
|
174
|
+
expiryEl.mount('#expiry');
|
|
175
|
+
cvvEl.mount('#cvv');
|
|
128
176
|
|
|
129
|
-
|
|
177
|
+
[cardNumberEl, expiryEl, cvvEl].forEach(el => {
|
|
178
|
+
el.on('ready', () => { readyCount++; checkReady(); });
|
|
179
|
+
});
|
|
130
180
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
181
|
+
async function pay() {
|
|
182
|
+
const { token, cvcSession, card } = await vault.createToken({
|
|
183
|
+
billing: { firstName: 'Jane', lastName: 'Smith' },
|
|
184
|
+
});
|
|
185
|
+
// POST { token, cvcSession } to your server
|
|
186
|
+
}
|
|
135
187
|
|
|
136
|
-
|
|
188
|
+
// Clean up when done
|
|
189
|
+
vault.destroy();
|
|
190
|
+
```
|
|
137
191
|
|
|
138
|
-
|
|
192
|
+
> **Gating the submit button in vanilla JS** requires checking both `vault.isReady` (tokenizer iframe loaded) **and** every element's `ready` event (input iframes loaded). In React, `useOzElements().ready` combines both automatically.
|
|
139
193
|
|
|
140
|
-
|
|
141
|
-
|---|---|
|
|
142
|
-
| **Method / URL** | `POST {vaultBaseUrl}/internal/wax-session` |
|
|
143
|
-
| **Auth** | `X-API-Key: <vault API key>` (server only) |
|
|
144
|
-
| **Body** | `{ "checkout_session_id": "<sessionId>" }` — optional; correlates with the SDK session. `{}` is valid. |
|
|
145
|
-
| **Success** | HTTP **201** · `{ "data": { "wax_key": "<uuid>", "expires_in_seconds": 1800 } }` (TTL is typically 30 minutes) |
|
|
146
|
-
| **Errors** | JSON such as `{ "message", "error_code" }` — treat like any API error |
|
|
194
|
+
---
|
|
147
195
|
|
|
148
|
-
|
|
196
|
+
## Server setup
|
|
149
197
|
|
|
150
|
-
###
|
|
198
|
+
### Wax key endpoint
|
|
151
199
|
|
|
152
|
-
|
|
200
|
+
The SDK calls your `fetchWaxKey` function with a `tokenizationSessionId` UUID. Your backend must exchange it for a wax key from the vault.
|
|
153
201
|
|
|
154
|
-
**Next.js App Router
|
|
202
|
+
**Next.js App Router (recommended)**
|
|
155
203
|
|
|
156
204
|
```ts
|
|
205
|
+
// app/api/mint-wax/route.ts
|
|
157
206
|
import { Ozura, createMintWaxHandler } from '@ozura/elements/server';
|
|
158
207
|
|
|
159
208
|
const ozura = new Ozura({
|
|
160
209
|
merchantId: process.env.MERCHANT_ID!,
|
|
161
210
|
apiKey: process.env.MERCHANT_API_KEY!,
|
|
162
211
|
vaultKey: process.env.VAULT_API_KEY!,
|
|
163
|
-
// vaultUrl: process.env.VAULT_URL, // optional override
|
|
164
212
|
});
|
|
165
213
|
|
|
166
214
|
export const POST = createMintWaxHandler(ozura);
|
|
167
215
|
```
|
|
168
216
|
|
|
169
|
-
**Express**
|
|
217
|
+
**Express**
|
|
170
218
|
|
|
171
219
|
```ts
|
|
172
220
|
import express from 'express';
|
|
173
221
|
import { Ozura, createMintWaxMiddleware } from '@ozura/elements/server';
|
|
174
222
|
|
|
223
|
+
const ozura = new Ozura({ merchantId: '...', apiKey: '...', vaultKey: '...' });
|
|
175
224
|
const app = express();
|
|
176
|
-
const ozura = new Ozura({
|
|
177
|
-
merchantId: process.env.MERCHANT_ID!,
|
|
178
|
-
apiKey: process.env.MERCHANT_API_KEY!,
|
|
179
|
-
vaultKey: process.env.VAULT_API_KEY!,
|
|
180
|
-
});
|
|
181
225
|
|
|
182
226
|
app.use(express.json());
|
|
183
227
|
app.post('/api/mint-wax', createMintWaxMiddleware(ozura));
|
|
184
228
|
```
|
|
185
229
|
|
|
186
|
-
|
|
230
|
+
**Manual implementation**
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
// POST /api/mint-wax
|
|
234
|
+
const { sessionId } = await req.json();
|
|
235
|
+
const { waxKey } = await ozura.mintWaxKey({
|
|
236
|
+
tokenizationSessionId: sessionId,
|
|
237
|
+
maxTokenizeCalls: 3, // vault enforces this limit; must match VaultOptions.maxTokenizeCalls on the client
|
|
238
|
+
});
|
|
239
|
+
return Response.json({ waxKey });
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Card sale endpoint
|
|
243
|
+
|
|
244
|
+
After `createToken()` resolves on the frontend, POST `{ token, cvcSession, billing }` to your server to charge the card.
|
|
187
245
|
|
|
188
|
-
|
|
246
|
+
**Next.js App Router**
|
|
189
247
|
|
|
190
248
|
```ts
|
|
191
|
-
app
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
)
|
|
198
|
-
|
|
199
|
-
|
|
249
|
+
// app/api/charge/route.ts
|
|
250
|
+
import { Ozura, createCardSaleHandler } from '@ozura/elements/server';
|
|
251
|
+
|
|
252
|
+
const ozura = new Ozura({ merchantId: '...', apiKey: '...', vaultKey: '...' });
|
|
253
|
+
|
|
254
|
+
export const POST = createCardSaleHandler(ozura, {
|
|
255
|
+
getAmount: async (body) => {
|
|
256
|
+
const order = await db.orders.findById(body.orderId as string);
|
|
257
|
+
return order.total; // decimal string, e.g. "49.00"
|
|
258
|
+
},
|
|
259
|
+
// getCurrency: async (body) => 'USD', // optional, defaults to "USD"
|
|
200
260
|
});
|
|
201
261
|
```
|
|
202
262
|
|
|
203
|
-
|
|
263
|
+
**Express**
|
|
204
264
|
|
|
205
|
-
|
|
265
|
+
```ts
|
|
266
|
+
app.post('/api/charge', createCardSaleMiddleware(ozura, {
|
|
267
|
+
getAmount: async (body) => {
|
|
268
|
+
const order = await db.orders.findById(body.orderId as string);
|
|
269
|
+
return order.total; // decimal string, e.g. "49.00"
|
|
270
|
+
},
|
|
271
|
+
// getCurrency: async (body) => 'USD', // optional, defaults to "USD"
|
|
272
|
+
}));
|
|
273
|
+
```
|
|
206
274
|
|
|
207
|
-
|
|
275
|
+
> `createCardSaleMiddleware` always terminates the request — it does not call `next()` and cannot be composed in a middleware chain.
|
|
208
276
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
277
|
+
**Manual implementation**
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
const { token, cvcSession, billing } = await req.json();
|
|
281
|
+
const result = await ozura.cardSale({
|
|
282
|
+
token,
|
|
283
|
+
cvcSession,
|
|
284
|
+
amount: '49.00',
|
|
285
|
+
currency: 'USD',
|
|
286
|
+
billing,
|
|
287
|
+
// Take the first IP from the forwarded-for chain; fall back to socket address.
|
|
288
|
+
// req is a Fetch API Request (Next.js App Router / Vercel Edge).
|
|
289
|
+
clientIpAddress: req.headers.get('x-forwarded-for')?.split(',')[0].trim()
|
|
290
|
+
?? req.headers.get('x-real-ip')
|
|
291
|
+
?? '',
|
|
292
|
+
});
|
|
293
|
+
// result.transactionId, result.amount, result.cardLastFour, result.cardBrand
|
|
223
294
|
```
|
|
224
295
|
|
|
225
|
-
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## Vanilla JS API
|
|
226
299
|
|
|
227
|
-
|
|
300
|
+
### OzVault.create(options)
|
|
228
301
|
|
|
229
|
-
|
|
302
|
+
```ts
|
|
303
|
+
const vault = await OzVault.create(options: VaultOptions): Promise<OzVault>
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Mounts the hidden tokenizer iframe and fetches the wax key concurrently. Both happen in parallel — by the time `create()` resolves, the iframe may already be ready.
|
|
307
|
+
|
|
308
|
+
| Option | Type | Required | Description |
|
|
309
|
+
|---|---|---|---|
|
|
310
|
+
| `pubKey` | `string` | ✓ | Your public key from the Ozura admin. |
|
|
311
|
+
| `fetchWaxKey` | `(sessionId: string) => Promise<string>` | ✓ | Called with the session ID; must return a wax key string from your server. Use `createFetchWaxKey('/api/mint-wax')` for the common case. |
|
|
312
|
+
| `frameBaseUrl` | `string` | — | Base URL for iframe assets. Defaults to production CDN. Override for local dev (see [Local development](#local-development)). |
|
|
313
|
+
| `fonts` | `FontSource[]` | — | Custom fonts to inject into all element iframes. |
|
|
314
|
+
| `appearance` | `Appearance` | — | Global theme and variable overrides. |
|
|
315
|
+
| `loadTimeoutMs` | `number` | — | Tokenizer iframe load timeout in ms. Default: `10000`. Only takes effect when `onLoadError` is also provided. |
|
|
316
|
+
| `onLoadError` | `() => void` | — | Called if the tokenizer iframe fails to load within `loadTimeoutMs`. |
|
|
317
|
+
| `onWaxRefresh` | `() => void` | — | Called when the SDK silently re-mints an expired wax key mid-tokenization. |
|
|
318
|
+
| `onReady` | `() => void` | — | Called once when the tokenizer iframe has loaded and is ready. Use in vanilla JS to re-check submit-button readiness when the tokenizer becomes ready after all element iframes have already fired. In React, `useOzElements().ready` handles this automatically. |
|
|
319
|
+
| `maxTokenizeCalls` | `number` | — | Maximum successful `createToken` calls per wax key before the key is considered consumed. Default: `3`. Must match `maxTokenizeCalls` in your server-side `mintWaxKey` call. |
|
|
230
320
|
|
|
231
|
-
|
|
321
|
+
Throws `OzError` if `fetchWaxKey` rejects, returns an empty string, or returns a non-string value.
|
|
232
322
|
|
|
233
|
-
|
|
323
|
+
> **`createFetchWaxKey` retry behavior:** The built-in helper enforces a **10-second per-attempt timeout** and retries **once after 750 ms on pure network failures** (connection refused, DNS failure, offline). HTTP 4xx/5xx errors are never retried — they signal endpoint misconfiguration or invalid credentials and require developer action. Errors are thrown as `OzError` instances so you can inspect `err.errorCode` and `err.retryable`.
|
|
234
324
|
|
|
235
|
-
|
|
325
|
+
> **Re-export identity:** `createFetchWaxKey` is exported from both `@ozura/elements` and `@ozura/elements/react`. They are identical — the same function. Use whichever matches your import context.
|
|
236
326
|
|
|
237
327
|
---
|
|
238
328
|
|
|
239
|
-
|
|
329
|
+
### vault.createElement(type, options?)
|
|
240
330
|
|
|
241
|
-
|
|
331
|
+
```ts
|
|
332
|
+
vault.createElement(type: ElementType, options?: ElementOptions): OzElement
|
|
333
|
+
```
|
|
242
334
|
|
|
243
|
-
|
|
335
|
+
Creates and returns an element iframe. Call `.mount(target)` to attach it to the DOM.
|
|
244
336
|
|
|
245
|
-
|
|
246
|
-
|---|---|---|---|
|
|
247
|
-
| `options.pubKey` | `string` | required | System pub key for the tokenize endpoint. Obtain from Ozura admin. |
|
|
248
|
-
| `options.fetchWaxKey` | `(sessionId: string) => Promise<string>` | required | Called once during `create()`. Receives an SDK-generated session UUID; your implementation passes it to your backend, which mints a session-bound wax key from the vault using your vault secret. Returns the wax key string. |
|
|
249
|
-
| `options.frameBaseUrl` | `string` | `https://elements.ozura.com` | Where iframe assets are served from |
|
|
250
|
-
| `options.fonts` | `FontSource[]` | `[]` | Custom fonts to inject into element iframes |
|
|
251
|
-
| `options.appearance` | `ElementStyleConfig` | — | Default style applied to all created elements |
|
|
252
|
-
| `options.onLoadError` | `() => void` | — | Called if the tokenizer iframe fails to load within `loadTimeoutMs` |
|
|
253
|
-
| `options.loadTimeoutMs` | `number` | `10000` | Timeout in ms before `onLoadError` fires |
|
|
337
|
+
`ElementType`: `'cardNumber'` | `'cvv'` | `'expirationDate'`
|
|
254
338
|
|
|
255
|
-
|
|
339
|
+
```ts
|
|
340
|
+
const cardEl = vault.createElement('cardNumber', {
|
|
341
|
+
placeholder: '1234 5678 9012 3456',
|
|
342
|
+
style: {
|
|
343
|
+
base: { color: '#1a1a1a', fontSize: '16px' },
|
|
344
|
+
focus: { borderColor: '#6366f1' },
|
|
345
|
+
invalid: { color: '#dc2626' },
|
|
346
|
+
complete: { color: '#16a34a' },
|
|
347
|
+
},
|
|
348
|
+
});
|
|
256
349
|
|
|
257
|
-
|
|
350
|
+
cardEl.mount('#card-number-container');
|
|
351
|
+
```
|
|
258
352
|
|
|
259
|
-
`
|
|
353
|
+
`ElementOptions`:
|
|
260
354
|
|
|
261
355
|
| Option | Type | Description |
|
|
262
356
|
|---|---|---|
|
|
263
|
-
| `style
|
|
264
|
-
| `
|
|
265
|
-
| `
|
|
266
|
-
| `
|
|
267
|
-
| `disabled` | `boolean` | Disable the input |
|
|
268
|
-
|
|
269
|
-
### `vault.createToken(options?)`
|
|
270
|
-
|
|
271
|
-
Triggers tokenization across all mounted elements. Returns a Promise with the vault token.
|
|
272
|
-
|
|
273
|
-
```ts
|
|
274
|
-
// Minimal — just tokenize the card data
|
|
275
|
-
const { token, cvcSession } = await vault.createToken();
|
|
276
|
-
|
|
277
|
-
// With billing — validates and normalizes billing details alongside tokenization.
|
|
278
|
-
// Useful if you plan to pass billing to a payment API (e.g. Ozura Pay API).
|
|
279
|
-
const { token, cvcSession, billing } = await vault.createToken({
|
|
280
|
-
billing: {
|
|
281
|
-
firstName: string; // required, 1–50 chars
|
|
282
|
-
lastName: string; // required, 1–50 chars
|
|
283
|
-
email?: string; // valid address, max 50 chars
|
|
284
|
-
phone?: string; // E.164 format e.g. "+15551234567"
|
|
285
|
-
address?: {
|
|
286
|
-
line1: string; // required if address provided
|
|
287
|
-
line2?: string; // omitted from output if blank
|
|
288
|
-
city: string;
|
|
289
|
-
state: string; // "California" auto-normalised → "CA" for US/CA
|
|
290
|
-
zip: string;
|
|
291
|
-
country: string; // ISO 3166-1 alpha-2, e.g. "US"
|
|
292
|
-
};
|
|
293
|
-
};
|
|
357
|
+
| `style` | `ElementStyleConfig` | Per-state style overrides. See [Styling](#styling). |
|
|
358
|
+
| `placeholder` | `string` | Placeholder text (max 100 characters). |
|
|
359
|
+
| `disabled` | `boolean` | Disables the input. |
|
|
360
|
+
| `loadTimeoutMs` | `number` | Iframe load timeout in ms. Default: `10000`. |
|
|
294
361
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
362
|
+
**OzElement methods:**
|
|
363
|
+
|
|
364
|
+
| Method | Description |
|
|
365
|
+
|---|---|
|
|
366
|
+
| `.mount(target)` | Mount the iframe. Accepts a CSS selector string or `HTMLElement`. |
|
|
367
|
+
| `.unmount()` | Remove the iframe from the DOM. The element can be re-mounted. |
|
|
368
|
+
| `.destroy()` | Permanently destroy the element. Cannot be re-mounted. |
|
|
369
|
+
| `.update(options)` | Update placeholder, style, or disabled state without re-mounting. **Merge semantics** — only provided keys are applied; omitted keys retain their values. To reset a property, pass it with an empty string (e.g. `{ style: { base: { color: '' } } }`). To fully reset all styles, destroy and recreate the element. |
|
|
370
|
+
| `.clear()` | Clear the field value. |
|
|
371
|
+
| `.focus()` | Programmatically focus the input. |
|
|
372
|
+
| `.blur()` | Programmatically blur the input. |
|
|
373
|
+
| `.on(event, fn)` | Subscribe to an event. Returns `this` for chaining. |
|
|
374
|
+
| `.off(event, fn)` | Remove an event handler. |
|
|
375
|
+
| `.once(event, fn)` | Subscribe for a single invocation. |
|
|
376
|
+
| `.isReady` | `true` once the iframe has loaded and signalled ready. |
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
### vault.createToken(options?)
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
vault.createToken(options?: TokenizeOptions): Promise<TokenResponse>
|
|
299
384
|
```
|
|
300
385
|
|
|
301
|
-
`
|
|
386
|
+
Tokenizes all mounted and ready card elements. Raw values travel directly from element iframes to the tokenizer iframe via `MessageChannel` — they never pass through your page's JavaScript.
|
|
302
387
|
|
|
303
|
-
|
|
304
|
-
|---|---|
|
|
305
|
-
| `token` | Vault token referencing the stored card data |
|
|
306
|
-
| `cvcSession` | CVC session ID for use with payment APIs |
|
|
307
|
-
| `billing` | Validated, normalized billing details (only present when `billing` was passed) |
|
|
388
|
+
Returns a `TokenResponse`:
|
|
308
389
|
|
|
309
|
-
|
|
390
|
+
```ts
|
|
391
|
+
interface TokenResponse {
|
|
392
|
+
token: string; // Vault token — pass to cardSale
|
|
393
|
+
cvcSession: string; // CVC session — always present; pass to cardSale
|
|
394
|
+
card: { // Card metadata — always present on success
|
|
395
|
+
last4: string; // e.g. "4242"
|
|
396
|
+
brand: string; // e.g. "visa"
|
|
397
|
+
expMonth: string; // e.g. "09"
|
|
398
|
+
expYear: string; // e.g. "2027"
|
|
399
|
+
};
|
|
400
|
+
billing?: BillingDetails; // Normalized billing — only present if billing was passed in
|
|
401
|
+
}
|
|
402
|
+
```
|
|
310
403
|
|
|
311
|
-
|
|
404
|
+
`TokenizeOptions`:
|
|
312
405
|
|
|
313
|
-
|
|
406
|
+
| Option | Type | Description |
|
|
407
|
+
|---|---|---|
|
|
408
|
+
| `billing` | `BillingDetails` | Validated and normalized billing details. Returned in `TokenResponse.billing`. |
|
|
409
|
+
| `firstName` | `string` | **Deprecated.** Pass inside `billing` instead. |
|
|
410
|
+
| `lastName` | `string` | **Deprecated.** Pass inside `billing` instead. |
|
|
314
411
|
|
|
315
|
-
|
|
412
|
+
Throws `OzError` if:
|
|
413
|
+
- The vault is not ready (`errorCode: 'unknown'`)
|
|
414
|
+
- A tokenization is already in progress
|
|
415
|
+
- Billing validation fails (`errorCode: 'validation'`)
|
|
416
|
+
- No elements are mounted
|
|
417
|
+
- The vault returns an error (`errorCode` reflects the HTTP status)
|
|
418
|
+
- The request times out after 30 seconds (`errorCode: 'timeout'`) — this timeout is separate from `loadTimeoutMs` and is not configurable
|
|
316
419
|
|
|
317
|
-
|
|
420
|
+
**`vault.tokenizeCount`**
|
|
318
421
|
|
|
319
|
-
```
|
|
320
|
-
//
|
|
321
|
-
return () => vault.destroy();
|
|
422
|
+
```ts
|
|
423
|
+
vault.tokenizeCount: number // read-only getter
|
|
322
424
|
```
|
|
323
425
|
|
|
324
|
-
|
|
426
|
+
Returns the number of successful `createToken()` / `createBankToken()` calls made against the current wax key. Resets to `0` each time the wax key is refreshed (proactively or reactively). Use this in vanilla JS to display "attempts remaining" feedback or gate the submit button:
|
|
325
427
|
|
|
326
|
-
|
|
428
|
+
```ts
|
|
429
|
+
const MAX = 3; // matches maxTokenizeCalls
|
|
430
|
+
const remaining = MAX - vault.tokenizeCount;
|
|
431
|
+
payButton.textContent = `Pay (${remaining} attempt${remaining === 1 ? '' : 's'} remaining)`;
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
In React, use `tokenizeCount` from `useOzElements()` instead — it is a reactive state value and will trigger re-renders automatically.
|
|
435
|
+
|
|
436
|
+
---
|
|
327
437
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
438
|
+
### vault.createBankElement()
|
|
439
|
+
|
|
440
|
+
```ts
|
|
441
|
+
vault.createBankElement(type: BankElementType, options?: ElementOptions): OzElement
|
|
331
442
|
```
|
|
332
443
|
|
|
333
|
-
|
|
444
|
+
Creates a bank account element. `BankElementType`: `'accountNumber'` | `'routingNumber'`.
|
|
334
445
|
|
|
335
|
-
|
|
446
|
+
```ts
|
|
447
|
+
const accountEl = vault.createBankElement('accountNumber');
|
|
448
|
+
const routingEl = vault.createBankElement('routingNumber');
|
|
449
|
+
accountEl.mount('#account-number');
|
|
450
|
+
routingEl.mount('#routing-number');
|
|
451
|
+
```
|
|
336
452
|
|
|
337
|
-
|
|
453
|
+
---
|
|
338
454
|
|
|
339
|
-
|
|
340
|
-
|---|---|---|
|
|
341
|
-
| `ready` | — | Iframe loaded and initialised |
|
|
342
|
-
| `change` | `ElementChangeEvent` | Input value changed |
|
|
343
|
-
| `focus` | — | Input focused |
|
|
344
|
-
| `blur` | — | Input blurred |
|
|
455
|
+
### vault.createBankToken(options)
|
|
345
456
|
|
|
346
|
-
|
|
457
|
+
```ts
|
|
458
|
+
vault.createBankToken(options: BankTokenizeOptions): Promise<BankTokenResponse>
|
|
459
|
+
```
|
|
347
460
|
|
|
348
|
-
|
|
349
|
-
|---|---|---|
|
|
350
|
-
| `empty` | `boolean` | all elements |
|
|
351
|
-
| `complete` | `boolean` | all elements |
|
|
352
|
-
| `valid` | `boolean` | all elements |
|
|
353
|
-
| `error` | `string \| undefined` | all — set when complete but invalid |
|
|
354
|
-
| `cardBrand` | `string \| undefined` | `cardNumber` only |
|
|
355
|
-
| `month` | `string \| undefined` | `expirationDate` only (`"01"`–`"12"`) |
|
|
356
|
-
| `year` | `string \| undefined` | `expirationDate` only (2-digit, e.g. `"27"`) |
|
|
461
|
+
Tokenizes the mounted `accountNumber` and `routingNumber` elements. Both must be mounted and ready.
|
|
357
462
|
|
|
358
|
-
|
|
463
|
+
```ts
|
|
464
|
+
interface BankTokenizeOptions {
|
|
465
|
+
firstName: string; // Account holder first name (required, max 50 chars)
|
|
466
|
+
lastName: string; // Account holder last name (required, max 50 chars)
|
|
467
|
+
}
|
|
359
468
|
|
|
360
|
-
|
|
469
|
+
interface BankTokenResponse {
|
|
470
|
+
token: string;
|
|
471
|
+
bank?: {
|
|
472
|
+
last4: string; // Last 4 digits of account number
|
|
473
|
+
routingNumberLast4: string;
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
```
|
|
361
477
|
|
|
362
|
-
|
|
478
|
+
> **Note:** OzuraPay does not currently support bank account payments. Use the bank token with your own ACH processor. Bank tokens can be passed to any ACH-capable processor directly.
|
|
363
479
|
|
|
364
|
-
|
|
480
|
+
> **Card tokens and processors:** Card tokenization returns a `cvcSession` alongside the `token`. OzuraPay's charge API requires `cvcSession`. If you are routing to a non-Ozura processor, pass the `token` directly — your processor's documentation will tell you whether a CVC session token is required.
|
|
365
481
|
|
|
366
482
|
---
|
|
367
483
|
|
|
368
|
-
|
|
484
|
+
### vault.destroy()
|
|
369
485
|
|
|
370
|
-
|
|
486
|
+
```ts
|
|
487
|
+
vault.destroy(): void
|
|
488
|
+
```
|
|
371
489
|
|
|
372
|
-
|
|
373
|
-
|--------|------|
|
|
374
|
-
| `new Ozura({ merchantId, apiKey, vaultKey, apiUrl?, vaultUrl?, … })` | Pay API (`cardSale`, `listTransactions`) and vault wax calls |
|
|
375
|
-
| `ozura.mintWaxKey({ tokenizationSessionId? })` | `POST /internal/wax-session` — implement or use helpers below |
|
|
376
|
-
| `ozura.revokeWaxKey(waxKey)` | Best-effort revoke when the checkout session ends |
|
|
377
|
-
| `createMintWaxHandler(ozura)` | Fetch API handler (Next.js App Router, Workers, Edge) → `{ waxKey }` |
|
|
378
|
-
| `createMintWaxMiddleware(ozura)` | Express / Connect middleware → `{ waxKey }` |
|
|
379
|
-
| `getClientIp(req)` | Helper for `cardSale` client IP |
|
|
490
|
+
Tears down the vault: removes all element and tokenizer iframes, clears the `message` event listener, rejects any pending `createToken()` / `createBankToken()` promises, and cancels active timeout handles. Call this when the checkout component unmounts.
|
|
380
491
|
|
|
381
|
-
|
|
492
|
+
```ts
|
|
493
|
+
// React useEffect cleanup — the cancel flag prevents a vault from leaking
|
|
494
|
+
// if the component unmounts before OzVault.create() resolves.
|
|
495
|
+
useEffect(() => {
|
|
496
|
+
let cancelled = false;
|
|
497
|
+
let vault: OzVault | null = null;
|
|
498
|
+
OzVault.create(options).then(v => {
|
|
499
|
+
if (cancelled) { v.destroy(); return; }
|
|
500
|
+
vault = v;
|
|
501
|
+
});
|
|
502
|
+
return () => {
|
|
503
|
+
cancelled = true;
|
|
504
|
+
vault?.destroy();
|
|
505
|
+
};
|
|
506
|
+
}, []);
|
|
507
|
+
```
|
|
382
508
|
|
|
383
509
|
---
|
|
384
510
|
|
|
385
|
-
|
|
511
|
+
### OzElement events
|
|
386
512
|
|
|
387
|
-
```
|
|
388
|
-
|
|
513
|
+
```ts
|
|
514
|
+
element.on('change', (event: ElementChangeEvent) => { ... });
|
|
515
|
+
element.on('focus', () => { ... });
|
|
516
|
+
element.on('blur', () => { ... });
|
|
517
|
+
element.on('ready', () => { ... });
|
|
518
|
+
element.on('loaderror', (payload: { elementType: string; error: string }) => { ... });
|
|
389
519
|
```
|
|
390
520
|
|
|
391
|
-
|
|
521
|
+
`ElementChangeEvent`:
|
|
522
|
+
|
|
523
|
+
| Field | Type | Description |
|
|
524
|
+
|---|---|---|
|
|
525
|
+
| `empty` | `boolean` | `true` when the field is empty. |
|
|
526
|
+
| `complete` | `boolean` | `true` when the field has enough digits to be complete. |
|
|
527
|
+
| `valid` | `boolean` | `true` when the value passes all validation (Luhn, expiry date, etc.). |
|
|
528
|
+
| `error` | `string \| undefined` | User-facing error message when `valid` is `false` and the field has been touched. |
|
|
529
|
+
| `cardBrand` | `string \| undefined` | Detected brand — only on `cardNumber` fields (e.g. `"visa"`, `"amex"`). |
|
|
530
|
+
| `month` | `string \| undefined` | Parsed 2-digit month — only on `expirationDate` fields. |
|
|
531
|
+
| `year` | `string \| undefined` | Parsed 2-digit year — only on `expirationDate` fields. |
|
|
532
|
+
|
|
533
|
+
> **Expiry data and PCI scope:** The expiry `onChange` event delivers parsed `month` and `year` to your handler for display purposes (e.g. "Card expires MM/YY"). These are not PANs or CVVs, but they are cardholder data. If your PCI scope requires zero cardholder data on the merchant page, do not read, log, or store these fields.
|
|
534
|
+
|
|
535
|
+
Auto-advance is built in: the vault automatically moves focus from card number → expiry → CVV when each field completes. No additional code required.
|
|
536
|
+
|
|
537
|
+
---
|
|
538
|
+
|
|
539
|
+
## React API
|
|
540
|
+
|
|
541
|
+
### OzElements provider
|
|
392
542
|
|
|
393
543
|
```tsx
|
|
394
|
-
import { OzElements,
|
|
544
|
+
import { OzElements, createFetchWaxKey } from '@ozura/elements/react';
|
|
545
|
+
|
|
546
|
+
<OzElements
|
|
547
|
+
pubKey="pk_live_..."
|
|
548
|
+
fetchWaxKey={createFetchWaxKey('/api/mint-wax')}
|
|
549
|
+
appearance={{ theme: 'flat', variables: { colorPrimary: '#6366f1' } }}
|
|
550
|
+
onLoadError={() => setPaymentUnavailable(true)}
|
|
551
|
+
>
|
|
552
|
+
{children}
|
|
553
|
+
</OzElements>
|
|
395
554
|
```
|
|
396
555
|
|
|
397
|
-
|
|
556
|
+
All `VaultOptions` are accepted as props. The provider creates a single `OzVault` instance and destroys it on unmount.
|
|
398
557
|
|
|
399
|
-
|
|
400
|
-
import { useState } from 'react';
|
|
401
|
-
import { OzElements, OzCardNumber, OzExpiry, OzCvv, useOzElements } from '@ozura/elements/react';
|
|
402
|
-
import { normalizeCardSaleError } from '@ozura/elements';
|
|
403
|
-
|
|
404
|
-
const fieldStyle = {
|
|
405
|
-
base: { color: '#111827', fontSize: '16px', fontFamily: 'Inter, sans-serif' },
|
|
406
|
-
focus: { color: '#111827' },
|
|
407
|
-
invalid: { color: '#dc2626' },
|
|
408
|
-
};
|
|
558
|
+
> **Prop changes and vault lifecycle:** Changing `pubKey`, `frameBaseUrl`, `loadTimeoutMs`, `appearance`, `fonts`, or `maxTokenizeCalls` destroys the current vault and creates a new one — all field iframes will remount. Changing `fetchWaxKey`, `onLoadError`, `onWaxRefresh`, or `onReady` updates the callback in place via refs without recreating the vault.
|
|
409
559
|
|
|
410
|
-
|
|
411
|
-
const { createToken, ready } = useOzElements();
|
|
560
|
+
> **One card form per provider:** A vault holds one element per field type (`cardNumber`, `expiry`, `cvv`, etc.). Rendering two `<OzCard>` components under the same `<OzElements>` provider will cause the second to silently replace the first's iframes, breaking the first form. If you genuinely need two independent card forms on the same page, wrap each in its own `<OzElements>` provider with separate `pubKey` / `fetchWaxKey` configurations.
|
|
412
561
|
|
|
413
|
-
|
|
414
|
-
firstName: '', lastName: '', email: '', phone: '',
|
|
415
|
-
address1: '', city: '', state: '', zip: '', country: 'US',
|
|
416
|
-
});
|
|
417
|
-
const [error, setError] = useState('');
|
|
418
|
-
|
|
419
|
-
const set = (field: string) => (e: React.ChangeEvent<HTMLInputElement>) =>
|
|
420
|
-
setForm(f => ({ ...f, [field]: e.target.value }));
|
|
421
|
-
|
|
422
|
-
const handlePay = async () => {
|
|
423
|
-
setError('');
|
|
424
|
-
try {
|
|
425
|
-
const { token, cvcSession, billing } = await createToken({
|
|
426
|
-
billing: {
|
|
427
|
-
firstName: form.firstName,
|
|
428
|
-
lastName: form.lastName,
|
|
429
|
-
email: form.email || undefined,
|
|
430
|
-
phone: form.phone || undefined,
|
|
431
|
-
...(form.address1 ? {
|
|
432
|
-
address: {
|
|
433
|
-
line1: form.address1,
|
|
434
|
-
city: form.city,
|
|
435
|
-
state: form.state,
|
|
436
|
-
zip: form.zip,
|
|
437
|
-
country: form.country.toUpperCase(),
|
|
438
|
-
},
|
|
439
|
-
} : {}),
|
|
440
|
-
},
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
// Send token + cvcSession + billing to your server → Ozura Pay API cardSale
|
|
444
|
-
const res = await fetch('/api/charge', {
|
|
445
|
-
method: 'POST',
|
|
446
|
-
headers: { 'Content-Type': 'application/json' },
|
|
447
|
-
body: JSON.stringify({ token, cvcSession, billing, amount: '49.00', currency: 'USD' }),
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
if (!res.ok) {
|
|
451
|
-
const data = await res.json();
|
|
452
|
-
setError(normalizeCardSaleError(data.error ?? 'Payment failed'));
|
|
453
|
-
}
|
|
454
|
-
} catch (err: unknown) {
|
|
455
|
-
setError(err instanceof Error ? err.message : 'An error occurred');
|
|
456
|
-
}
|
|
457
|
-
};
|
|
562
|
+
---
|
|
458
563
|
|
|
459
|
-
|
|
460
|
-
<div>
|
|
461
|
-
{/* Billing fields — plain inputs, not PCI-sensitive */}
|
|
462
|
-
<input placeholder="First name" value={form.firstName} onChange={set('firstName')} />
|
|
463
|
-
<input placeholder="Last name" value={form.lastName} onChange={set('lastName')} />
|
|
464
|
-
<input placeholder="Email" value={form.email} onChange={set('email')} />
|
|
465
|
-
<input placeholder="Phone (+15551234567)" value={form.phone} onChange={set('phone')} />
|
|
466
|
-
<input placeholder="Street address" value={form.address1} onChange={set('address1')} />
|
|
467
|
-
<input placeholder="City" value={form.city} onChange={set('city')} />
|
|
468
|
-
<input placeholder="State" value={form.state} onChange={set('state')} />
|
|
469
|
-
<input placeholder="ZIP" value={form.zip} onChange={set('zip')} />
|
|
470
|
-
<input placeholder="Country (US)" value={form.country} onChange={set('country')} maxLength={2} />
|
|
471
|
-
|
|
472
|
-
{/* Card fields — iframe-isolated, raw card data never touches this component */}
|
|
473
|
-
<OzCardNumber style={fieldStyle} />
|
|
474
|
-
<OzExpiry style={fieldStyle} />
|
|
475
|
-
<OzCvv style={fieldStyle} />
|
|
476
|
-
|
|
477
|
-
{error && <p style={{ color: 'red' }}>{error}</p>}
|
|
478
|
-
<button onClick={handlePay} disabled={!ready}>Pay $49.00</button>
|
|
479
|
-
</div>
|
|
480
|
-
);
|
|
481
|
-
}
|
|
564
|
+
### OzCard
|
|
482
565
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
566
|
+
Drop-in combined card component. Renders card number, expiry, and CVV with a configurable layout.
|
|
567
|
+
|
|
568
|
+
```tsx
|
|
569
|
+
import { OzCard } from '@ozura/elements/react';
|
|
570
|
+
|
|
571
|
+
<OzCard
|
|
572
|
+
layout="default" // "default" (number on top, expiry+CVV below) | "rows" (stacked)
|
|
573
|
+
onChange={(state) => {
|
|
574
|
+
// state.complete — all three fields complete + valid
|
|
575
|
+
// state.cardBrand — detected brand
|
|
576
|
+
// state.error — first error across all fields
|
|
577
|
+
// state.fields — per-field ElementChangeEvent objects
|
|
578
|
+
}}
|
|
579
|
+
onReady={() => console.log('all card fields loaded')}
|
|
580
|
+
disabled={isSubmitting}
|
|
581
|
+
labels={{ cardNumber: 'Card Number', expiry: 'Expiry', cvv: 'CVV' }}
|
|
582
|
+
placeholders={{ cardNumber: '1234 5678 9012 3456', expiry: 'MM/YY', cvv: '···' }}
|
|
583
|
+
/>
|
|
501
584
|
```
|
|
502
585
|
|
|
503
|
-
|
|
586
|
+
`OzCardProps` (full):
|
|
504
587
|
|
|
505
588
|
| Prop | Type | Description |
|
|
506
589
|
|---|---|---|
|
|
507
|
-
| `
|
|
508
|
-
| `
|
|
509
|
-
| `
|
|
510
|
-
| `
|
|
511
|
-
| `
|
|
512
|
-
| `
|
|
513
|
-
| `
|
|
590
|
+
| `layout` | `'default' \| 'rows'` | `'default'`: number full-width, expiry+CVV side by side. `'rows'`: all stacked. |
|
|
591
|
+
| `gap` | `number \| string` | Gap between fields. Default: `8` (px). |
|
|
592
|
+
| `style` | `ElementStyleConfig` | Shared style applied to all three inputs. |
|
|
593
|
+
| `styles` | `{ cardNumber?, expiry?, cvv? }` | Per-field overrides merged on top of `style`. |
|
|
594
|
+
| `classNames` | `{ cardNumber?, expiry?, cvv?, row? }` | CSS class names for field wrappers and the expiry+CVV row. |
|
|
595
|
+
| `labels` | `{ cardNumber?, expiry?, cvv? }` | Optional label text above each field. |
|
|
596
|
+
| `labelStyle` | `React.CSSProperties` | Style applied to all `<label>` elements. |
|
|
597
|
+
| `labelClassName` | `string` | Class applied to all `<label>` elements. |
|
|
598
|
+
| `placeholders` | `{ cardNumber?, expiry?, cvv? }` | Custom placeholder text per field. |
|
|
599
|
+
| `hideErrors` | `boolean` | Suppress the built-in error display. Handle via `onChange`. |
|
|
600
|
+
| `errorStyle` | `React.CSSProperties` | Style for the built-in error container. |
|
|
601
|
+
| `errorClassName` | `string` | Class for the built-in error container. |
|
|
602
|
+
| `renderError` | `(error: string) => ReactNode` | Custom error renderer. |
|
|
603
|
+
| `onChange` | `(state: OzCardState) => void` | Fires on any field change. |
|
|
604
|
+
| `onReady` | `() => void` | Fires once all three iframes have loaded. |
|
|
605
|
+
| `onFocus` | `(field: 'cardNumber' \| 'expiry' \| 'cvv') => void` | |
|
|
606
|
+
| `onBlur` | `(field: 'cardNumber' \| 'expiry' \| 'cvv') => void` | |
|
|
607
|
+
| `disabled` | `boolean` | Disable all inputs. |
|
|
608
|
+
| `className` | `string` | Class for the outer wrapper. |
|
|
609
|
+
|
|
610
|
+
---
|
|
514
611
|
|
|
515
|
-
###
|
|
612
|
+
### Individual field components
|
|
516
613
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
614
|
+
For custom layouts where `OzCard` is too opinionated:
|
|
615
|
+
|
|
616
|
+
```tsx
|
|
617
|
+
import { OzCardNumber, OzExpiry, OzCvv } from '@ozura/elements/react';
|
|
618
|
+
|
|
619
|
+
<OzCardNumber onChange={handleChange} placeholder="Card number" />
|
|
620
|
+
<OzExpiry onChange={handleChange} />
|
|
621
|
+
<OzCvv onChange={handleChange} />
|
|
622
|
+
```
|
|
521
623
|
|
|
522
|
-
|
|
624
|
+
All accept `OzFieldProps`:
|
|
523
625
|
|
|
524
626
|
| Prop | Type | Description |
|
|
525
627
|
|---|---|---|
|
|
526
|
-
| `style` | `ElementStyleConfig
|
|
527
|
-
| `placeholder` | `string
|
|
528
|
-
| `disabled` | `boolean
|
|
529
|
-
| `
|
|
530
|
-
| `
|
|
531
|
-
| `
|
|
532
|
-
| `
|
|
533
|
-
| `
|
|
628
|
+
| `style` | `ElementStyleConfig` | Input styles. |
|
|
629
|
+
| `placeholder` | `string` | Placeholder text. |
|
|
630
|
+
| `disabled` | `boolean` | Disables the input. |
|
|
631
|
+
| `loadTimeoutMs` | `number` | Iframe load timeout in ms. |
|
|
632
|
+
| `onChange` | `(event: ElementChangeEvent) => void` | |
|
|
633
|
+
| `onFocus` | `() => void` | |
|
|
634
|
+
| `onBlur` | `() => void` | |
|
|
635
|
+
| `onReady` | `() => void` | |
|
|
636
|
+
| `onLoadError` | `(error: string) => void` | |
|
|
637
|
+
| `className` | `string` | Class for the outer wrapper div. |
|
|
534
638
|
|
|
535
639
|
---
|
|
536
640
|
|
|
537
|
-
|
|
641
|
+
### OzBankCard
|
|
538
642
|
|
|
539
|
-
|
|
643
|
+
```tsx
|
|
644
|
+
import { OzBankCard } from '@ozura/elements/react';
|
|
645
|
+
|
|
646
|
+
<OzBankCard
|
|
647
|
+
onChange={(state) => {
|
|
648
|
+
// state.complete, state.error, state.fields.accountNumber, state.fields.routingNumber
|
|
649
|
+
}}
|
|
650
|
+
labels={{ accountNumber: 'Account Number', routingNumber: 'Routing Number' }}
|
|
651
|
+
/>
|
|
652
|
+
```
|
|
540
653
|
|
|
541
|
-
|
|
542
|
-
|-----------|---------------|
|
|
543
|
-
| Vault API key | **Server only.** Browser calls `fetchWaxKey` → your backend mints a wax key with this secret; never send the secret in the browser. Pay API `cardSale` also sends it as `vault-api-key` from the server. |
|
|
544
|
-
| Pay API key | Backend only: `x-api-key` header |
|
|
545
|
-
| Merchant ID | Backend only: `merchantId` in request body |
|
|
654
|
+
Or use individual bank components:
|
|
546
655
|
|
|
547
|
-
|
|
656
|
+
```tsx
|
|
657
|
+
import { OzBankAccountNumber, OzBankRoutingNumber } from '@ozura/elements/react';
|
|
548
658
|
|
|
549
|
-
|
|
659
|
+
<OzBankAccountNumber onChange={handleChange} />
|
|
660
|
+
<OzBankRoutingNumber onChange={handleChange} />
|
|
661
|
+
```
|
|
550
662
|
|
|
551
|
-
|
|
552
|
-
|
|
663
|
+
---
|
|
664
|
+
|
|
665
|
+
### useOzElements()
|
|
666
|
+
|
|
667
|
+
```ts
|
|
668
|
+
const { createToken, createBankToken, ready, initError, tokenizeCount } = useOzElements();
|
|
669
|
+
```
|
|
553
670
|
|
|
671
|
+
Must be called from inside an `<OzElements>` provider tree.
|
|
672
|
+
|
|
673
|
+
| Return | Type | Description |
|
|
674
|
+
|---|---|---|
|
|
675
|
+
| `createToken` | `(options?: TokenizeOptions) => Promise<TokenResponse>` | Tokenize mounted card elements. |
|
|
676
|
+
| `createBankToken` | `(options: BankTokenizeOptions) => Promise<BankTokenResponse>` | Tokenize mounted bank elements. |
|
|
677
|
+
| `ready` | `boolean` | `true` when the tokenizer **and** all mounted element iframes are ready. Gate your submit button on this. See note below. |
|
|
678
|
+
| `initError` | `Error \| null` | Non-null if `OzVault.create()` failed (e.g. `fetchWaxKey` threw). Render a fallback UI. |
|
|
679
|
+
| `tokenizeCount` | `number` | Number of successful tokenizations since the last wax key was minted. Resets on wax refresh or provider re-init. Useful for tracking calls against `maxTokenizeCalls`. |
|
|
680
|
+
|
|
681
|
+
> **`ready` vs `vault.isReady`:** `ready` from `useOzElements()` is a composite — it combines `vault.isReady` (tokenizer loaded) with element readiness (all mounted input iframes loaded). `vault.isReady` alone is insufficient for gating a submit button. Always use `ready` from `useOzElements()` in React.
|
|
682
|
+
|
|
683
|
+
---
|
|
684
|
+
|
|
685
|
+
## Styling
|
|
686
|
+
|
|
687
|
+
### Per-element styles
|
|
688
|
+
|
|
689
|
+
Styles apply inside each iframe's `<input>` element. Only an explicit allowlist of CSS properties is accepted (typography, spacing, borders, box-shadow, cursor, transitions). Values containing `url()`, `var()`, `expression()`, `javascript:`, or CSS breakout characters are blocked on both the SDK and iframe sides. Use literal values instead of CSS custom properties (`var(--token)` is rejected). When a value is stripped, the browser falls back to the element's default (unstyled) appearance for that property — the failure is silent with no error or console warning. Resolve design tokens to literal values before passing them in.
|
|
690
|
+
|
|
691
|
+
```ts
|
|
692
|
+
const style: ElementStyleConfig = {
|
|
693
|
+
base: {
|
|
694
|
+
color: '#1a1a1a',
|
|
695
|
+
fontSize: '16px',
|
|
696
|
+
fontFamily: '"Inter", sans-serif',
|
|
697
|
+
padding: '10px 12px',
|
|
698
|
+
backgroundColor: '#ffffff',
|
|
699
|
+
borderRadius: '6px',
|
|
700
|
+
border: '1px solid #d1d5db',
|
|
701
|
+
},
|
|
702
|
+
focus: {
|
|
703
|
+
borderColor: '#6366f1',
|
|
704
|
+
boxShadow: '0 0 0 3px rgba(99,102,241,0.15)',
|
|
705
|
+
outline: 'none',
|
|
706
|
+
},
|
|
707
|
+
invalid: {
|
|
708
|
+
borderColor: '#ef4444',
|
|
709
|
+
color: '#dc2626',
|
|
710
|
+
},
|
|
711
|
+
complete: {
|
|
712
|
+
borderColor: '#22c55e',
|
|
713
|
+
},
|
|
714
|
+
placeholder: {
|
|
715
|
+
color: '#9ca3af',
|
|
716
|
+
},
|
|
717
|
+
};
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
State precedence: `placeholder` applies to the `::placeholder` pseudo-element. `focus`, `invalid`, and `complete` merge on top of `base`.
|
|
721
|
+
|
|
722
|
+
### Global appearance
|
|
723
|
+
|
|
724
|
+
Apply a preset theme and/or variable overrides to all elements at once:
|
|
725
|
+
|
|
726
|
+
```ts
|
|
727
|
+
// OzVault.create
|
|
554
728
|
const vault = await OzVault.create({
|
|
555
|
-
pubKey: '
|
|
556
|
-
fetchWaxKey:
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
729
|
+
pubKey: '...',
|
|
730
|
+
fetchWaxKey: createFetchWaxKey('/api/mint-wax'),
|
|
731
|
+
appearance: {
|
|
732
|
+
theme: 'flat', // 'default' | 'night' | 'flat'
|
|
733
|
+
variables: {
|
|
734
|
+
colorText: '#1a1a1a',
|
|
735
|
+
colorBackground: '#ffffff',
|
|
736
|
+
colorPrimary: '#6366f1', // focus caret + color
|
|
737
|
+
colorDanger: '#dc2626', // invalid state
|
|
738
|
+
colorSuccess: '#16a34a', // complete state
|
|
739
|
+
colorPlaceholder: '#9ca3af',
|
|
740
|
+
fontFamily: '"Inter", sans-serif',
|
|
741
|
+
fontSize: '15px',
|
|
742
|
+
fontWeight: '400',
|
|
743
|
+
padding: '10px 14px',
|
|
744
|
+
letterSpacing: '0.01em',
|
|
745
|
+
},
|
|
563
746
|
},
|
|
564
747
|
});
|
|
565
748
|
|
|
566
|
-
//
|
|
749
|
+
// React provider
|
|
750
|
+
<OzElements pubKey="..." fetchWaxKey={...} appearance={{ theme: 'night' }}>
|
|
751
|
+
```
|
|
567
752
|
|
|
568
|
-
|
|
569
|
-
let tokenResult;
|
|
570
|
-
try {
|
|
571
|
-
tokenResult = await vault.createToken({
|
|
572
|
-
billing: {
|
|
573
|
-
firstName: formValues.firstName, // validated 1–50 chars
|
|
574
|
-
lastName: formValues.lastName,
|
|
575
|
-
email: formValues.email, // validated email format
|
|
576
|
-
phone: formValues.phone, // must be E.164: "+15551234567"
|
|
577
|
-
address: {
|
|
578
|
-
line1: formValues.address1,
|
|
579
|
-
line2: formValues.address2, // omitted from output if blank
|
|
580
|
-
city: formValues.city,
|
|
581
|
-
state: formValues.state, // "California" → "CA" auto-normalized
|
|
582
|
-
zip: formValues.zip,
|
|
583
|
-
country: formValues.country, // "US", "CA", "GB" …
|
|
584
|
-
},
|
|
585
|
-
},
|
|
586
|
-
});
|
|
587
|
-
} catch (err) {
|
|
588
|
-
// Validation errors thrown here — show directly to user
|
|
589
|
-
showError(err.message);
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
753
|
+
Per-element `style` takes precedence over `appearance` variables.
|
|
592
754
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
method: 'POST',
|
|
602
|
-
headers: { 'Content-Type': 'application/json' },
|
|
603
|
-
body: JSON.stringify({
|
|
604
|
-
ozuraCardToken: token,
|
|
605
|
-
ozuraCvcSession: cvcSession,
|
|
606
|
-
billingFirstName: billing.firstName,
|
|
607
|
-
billingLastName: billing.lastName,
|
|
608
|
-
billingEmail: billing.email,
|
|
609
|
-
billingPhone: billing.phone, // already E.164
|
|
610
|
-
billingAddress1: billing.address.line1,
|
|
611
|
-
billingAddress2: billing.address.line2, // omit key if undefined
|
|
612
|
-
billingCity: billing.address.city,
|
|
613
|
-
billingState: billing.address.state, // already 2-letter
|
|
614
|
-
billingZipcode: billing.address.zip,
|
|
615
|
-
billingCountry: billing.address.country,
|
|
616
|
-
clientIpAddress: ip,
|
|
617
|
-
amount: '49.00',
|
|
618
|
-
currency: 'USD',
|
|
619
|
-
salesTaxExempt: false,
|
|
620
|
-
surchargePercent: '0.00',
|
|
621
|
-
tipAmount: '0.00',
|
|
622
|
-
}),
|
|
623
|
-
});
|
|
755
|
+
> **`appearance: {}` is not "no theme":** Passing `appearance: {}` or `appearance: { variables: { ... } }` without a `theme` key applies the `'default'` preset as a base. To render elements with no preset styling, omit `appearance` entirely and use per-element `style` overrides.
|
|
756
|
+
>
|
|
757
|
+
> | `appearance` value | Result |
|
|
758
|
+
> |---|---|
|
|
759
|
+
> | *(omitted entirely)* | No preset — element uses minimal built-in defaults |
|
|
760
|
+
> | `{}` | Equivalent to `{ theme: 'default' }` — full default theme applied |
|
|
761
|
+
> | `{ theme: 'night' }` | Night theme |
|
|
762
|
+
> | `{ variables: { colorText: '#333' } }` | Default theme + variable overrides |
|
|
624
763
|
|
|
625
|
-
|
|
764
|
+
### Custom fonts
|
|
626
765
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
}
|
|
766
|
+
Fonts are injected into each iframe so they render inside the input fields:
|
|
767
|
+
|
|
768
|
+
```ts
|
|
769
|
+
fonts: [
|
|
770
|
+
// Google Fonts or any HTTPS CSS URL
|
|
771
|
+
{ cssSrc: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap' },
|
|
772
|
+
|
|
773
|
+
// Custom @font-face
|
|
774
|
+
{
|
|
775
|
+
family: 'BrandFont',
|
|
776
|
+
src: 'url(https://cdn.example.com/brand-font.woff2)',
|
|
777
|
+
weight: '400',
|
|
778
|
+
style: 'normal',
|
|
779
|
+
display: 'swap',
|
|
780
|
+
},
|
|
781
|
+
]
|
|
633
782
|
```
|
|
634
783
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
billingLastName: body.billingLastName,
|
|
655
|
-
billingEmail: body.billingEmail,
|
|
656
|
-
billingPhone: body.billingPhone,
|
|
657
|
-
billingAddress1: body.billingAddress1,
|
|
658
|
-
...(body.billingAddress2 ? { billingAddress2: body.billingAddress2 } : {}),
|
|
659
|
-
billingCity: body.billingCity,
|
|
660
|
-
billingState: body.billingState,
|
|
661
|
-
billingZipcode: body.billingZipcode,
|
|
662
|
-
billingCountry: body.billingCountry,
|
|
663
|
-
clientIpAddress: body.clientIpAddress,
|
|
664
|
-
salesTaxExempt: body.salesTaxExempt,
|
|
665
|
-
surchargePercent: '0.00',
|
|
666
|
-
tipAmount: '0.00',
|
|
784
|
+
Font `src` values must start with `url(https://...)`. HTTP and data URIs are rejected.
|
|
785
|
+
|
|
786
|
+
---
|
|
787
|
+
|
|
788
|
+
## Billing details
|
|
789
|
+
|
|
790
|
+
```ts
|
|
791
|
+
interface BillingDetails {
|
|
792
|
+
firstName: string; // 1–50 characters
|
|
793
|
+
lastName: string; // 1–50 characters
|
|
794
|
+
email?: string; // Valid email, max 50 characters
|
|
795
|
+
phone?: string; // E.164 format, e.g. "+15551234567", max 50 characters
|
|
796
|
+
address?: {
|
|
797
|
+
line1: string; // 1–50 characters
|
|
798
|
+
line2?: string; // Optional, omitted from cardSale if blank
|
|
799
|
+
city: string; // 1–50 characters
|
|
800
|
+
state: string; // For US/CA: normalized to 2-letter abbreviation
|
|
801
|
+
zip: string; // 1–50 characters
|
|
802
|
+
country: string; // ISO 3166-1 alpha-2, e.g. "US"
|
|
667
803
|
};
|
|
804
|
+
}
|
|
805
|
+
```
|
|
668
806
|
|
|
669
|
-
|
|
670
|
-
method: 'POST',
|
|
671
|
-
headers: {
|
|
672
|
-
'Content-Type': 'application/json',
|
|
673
|
-
'x-api-key': process.env.MERCHANT_API_KEY, // merchant Pay API key
|
|
674
|
-
'vault-api-key': process.env.VAULT_API_KEY, // same key used in OzVault
|
|
675
|
-
},
|
|
676
|
-
body: JSON.stringify(payload),
|
|
677
|
-
signal: AbortSignal.timeout(30_000),
|
|
678
|
-
});
|
|
807
|
+
Billing is validated and normalized by both `vault.createToken()` and the server-side handler factories. `TokenResponse.billing` contains the normalized result ready to spread into a `cardSale` call.
|
|
679
808
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
return res.status(response.status).json({ error: err.error ?? 'cardSale failed' });
|
|
684
|
-
}
|
|
809
|
+
---
|
|
810
|
+
|
|
811
|
+
## Error handling
|
|
685
812
|
|
|
686
|
-
|
|
813
|
+
All SDK errors are instances of `OzError`:
|
|
687
814
|
|
|
688
|
-
|
|
689
|
-
|
|
815
|
+
```ts
|
|
816
|
+
import { OzError } from '@ozura/elements';
|
|
817
|
+
|
|
818
|
+
try {
|
|
819
|
+
const { token } = await vault.createToken({ billing });
|
|
820
|
+
} catch (err) {
|
|
821
|
+
if (err instanceof OzError) {
|
|
822
|
+
switch (err.errorCode) {
|
|
823
|
+
case 'network': // Connection failure — show retry UI
|
|
824
|
+
case 'timeout': // 30s deadline exceeded — safe to retry
|
|
825
|
+
case 'server': // 5xx from vault — transient, safe to retry
|
|
826
|
+
if (err.retryable) showRetryPrompt();
|
|
827
|
+
break;
|
|
828
|
+
|
|
829
|
+
case 'auth': // Bad pub key / API key — configuration issue
|
|
830
|
+
case 'validation': // Bad card data — show field-level error
|
|
831
|
+
case 'config': // frameBaseUrl not in permitted allowlist
|
|
832
|
+
case 'unknown':
|
|
833
|
+
showError(err.message);
|
|
834
|
+
break;
|
|
835
|
+
}
|
|
690
836
|
}
|
|
837
|
+
}
|
|
838
|
+
```
|
|
691
839
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
// ⚠️ Store txn.transactionId in your database NOW — before responding to the client.
|
|
695
|
-
// You need it for refunds and to prevent duplicate charges on network retry.
|
|
696
|
-
await db.transactions.insertOne({
|
|
697
|
-
transactionId: txn.transactionId,
|
|
698
|
-
amount: txn.amount,
|
|
699
|
-
currency: txn.currency,
|
|
700
|
-
cardLastFour: txn.cardLastFour,
|
|
701
|
-
cardBrand: txn.cardBrand,
|
|
702
|
-
cardExpMonth: txn.cardExpMonth,
|
|
703
|
-
cardExpYear: txn.cardExpYear,
|
|
704
|
-
billingName: `${txn.billingFirstName} ${txn.billingLastName}`,
|
|
705
|
-
transDate: txn.transDate,
|
|
706
|
-
createdAt: new Date(),
|
|
707
|
-
});
|
|
840
|
+
`OzError` fields:
|
|
708
841
|
|
|
709
|
-
|
|
710
|
-
|
|
842
|
+
| Field | Type | Description |
|
|
843
|
+
|---|---|---|
|
|
844
|
+
| `message` | `string` | Human-readable, consumer-facing error message. |
|
|
845
|
+
| `errorCode` | `OzErrorCode` | `'network' \| 'timeout' \| 'auth' \| 'validation' \| 'server' \| 'config' \| 'unknown'` |
|
|
846
|
+
| `raw` | `string` | Raw error string from the vault API, if available. |
|
|
847
|
+
| `retryable` | `boolean` | `true` for `network`, `timeout`, `server`. `false` for `auth`, `validation`, `config`, `unknown`. |
|
|
848
|
+
|
|
849
|
+
> **Wax key expiry is handled automatically.** When a wax key expires or is consumed between initialization and the user clicking Pay, the SDK silently re-mints a fresh key and retries the tokenization once. You will only receive an `auth` error if the re-mint itself fails — for example, if your `/api/mint-wax` backend endpoint is unreachable. A healthy `auth` error in production means your mint endpoint needs attention, not that the user's card is bad.
|
|
850
|
+
|
|
851
|
+
**Error normalisation helpers** (for displaying errors from `cardSale` to users):
|
|
852
|
+
|
|
853
|
+
```ts
|
|
854
|
+
import { normalizeVaultError, normalizeBankVaultError, normalizeCardSaleError } from '@ozura/elements';
|
|
855
|
+
|
|
856
|
+
// Maps vault tokenize error strings to user-facing copy
|
|
857
|
+
const display = normalizeVaultError(err.raw); // card flows
|
|
858
|
+
const display = normalizeBankVaultError(err.raw); // bank/ACH flows
|
|
859
|
+
const display = normalizeCardSaleError(err.message); // cardSale API errors
|
|
711
860
|
```
|
|
712
861
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
//
|
|
727
|
-
//
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
// cardBrand: "visa",
|
|
731
|
-
// transDate: "2026-02-20T...",
|
|
732
|
-
// ozuraMerchantId:"...",
|
|
733
|
-
// billingFirstName, billingLastName, billingEmail, billingPhone,
|
|
734
|
-
// billingAddress1, billingAddress2?, billingCity, billingState,
|
|
735
|
-
// billingZipcode, billingCountry
|
|
736
|
-
// }
|
|
737
|
-
// }
|
|
738
|
-
//
|
|
739
|
-
// Error response (4xx/5xx): { error: string }
|
|
740
|
-
// → pass error to normalizeCardSaleError() for a user-facing message
|
|
741
|
-
```
|
|
742
|
-
|
|
743
|
-
### 3 — Client IP endpoint
|
|
744
|
-
|
|
745
|
-
The Pay API requires a real IP address. Fetch it server-side — do **not** use `api.ipify.org` from the client (blocked by most ad-blockers):
|
|
746
|
-
|
|
747
|
-
```js
|
|
748
|
-
app.get('/api/client-ip', (req, res) => {
|
|
749
|
-
const ip =
|
|
750
|
-
req.headers['x-forwarded-for']?.split(',')[0].trim() ??
|
|
751
|
-
req.socket.remoteAddress ??
|
|
752
|
-
'0.0.0.0';
|
|
753
|
-
res.json({ ip });
|
|
862
|
+
---
|
|
863
|
+
|
|
864
|
+
## Server utilities
|
|
865
|
+
|
|
866
|
+
### Ozura class
|
|
867
|
+
|
|
868
|
+
```ts
|
|
869
|
+
import { Ozura, OzuraError } from '@ozura/elements/server';
|
|
870
|
+
|
|
871
|
+
const ozura = new Ozura({
|
|
872
|
+
merchantId: process.env.MERCHANT_ID!,
|
|
873
|
+
apiKey: process.env.MERCHANT_API_KEY!,
|
|
874
|
+
vaultKey: process.env.VAULT_API_KEY!,
|
|
875
|
+
// apiUrl: 'https://api.ozura.com', // override Pay API URL
|
|
876
|
+
// vaultUrl: 'https://vault.ozura.com', // override vault URL
|
|
877
|
+
timeoutMs: 30000, // default
|
|
878
|
+
retries: 2, // max retry attempts for 5xx/network errors (3 total attempts)
|
|
754
879
|
});
|
|
755
880
|
```
|
|
756
881
|
|
|
757
|
-
|
|
882
|
+
> **Tokenize-only integrations** (mint wax keys + tokenize cards, no charging) only need `vaultKey`. The `merchantId` and `apiKey` fields are optional — they are validated lazily and only required when `cardSale()` is called.
|
|
883
|
+
>
|
|
884
|
+
> ```ts
|
|
885
|
+
> const ozura = new Ozura({ vaultKey: process.env.VAULT_API_KEY! });
|
|
886
|
+
> ```
|
|
758
887
|
|
|
759
|
-
|
|
888
|
+
**Methods:**
|
|
760
889
|
|
|
761
|
-
```
|
|
762
|
-
|
|
890
|
+
```ts
|
|
891
|
+
// Charge a tokenized card
|
|
892
|
+
const result = await ozura.cardSale({
|
|
893
|
+
token: tokenResponse.token,
|
|
894
|
+
cvcSession: tokenResponse.cvcSession,
|
|
895
|
+
amount: '49.00',
|
|
896
|
+
currency: 'USD', // default: 'USD'
|
|
897
|
+
billing: tokenResponse.billing,
|
|
898
|
+
clientIpAddress: '1.2.3.4', // fetch server-side, never from the browser
|
|
899
|
+
// surchargePercent, tipAmount, salesTaxExempt, processor
|
|
900
|
+
});
|
|
901
|
+
// result.transactionId, result.amount, result.cardLastFour, result.cardBrand
|
|
902
|
+
// result.surchargeAmount and result.tipAmount are optional — only present when non-zero
|
|
903
|
+
const surcharge = result.surchargeAmount ?? '0.00';
|
|
904
|
+
const tip = result.tipAmount ?? '0.00';
|
|
905
|
+
|
|
906
|
+
// Mint a wax key (for custom fetchWaxKey implementations)
|
|
907
|
+
const { waxKey, expiresInSeconds } = await ozura.mintWaxKey({
|
|
908
|
+
tokenizationSessionId: sessionId,
|
|
909
|
+
maxTokenizeCalls: 3, // must match VaultOptions.maxTokenizeCalls on the client (default: 3)
|
|
910
|
+
});
|
|
763
911
|
|
|
764
|
-
|
|
765
|
-
//
|
|
912
|
+
// Revoke a wax key — call on all three session-end paths
|
|
913
|
+
// Best-effort — never throws. Shortens the exposure window before the vault's ~30 min TTL.
|
|
914
|
+
await ozura.revokeWaxKey(waxKey);
|
|
766
915
|
|
|
767
|
-
|
|
768
|
-
//
|
|
916
|
+
// Suggested pattern — wire all three exit paths:
|
|
917
|
+
// 1. Payment success
|
|
918
|
+
const result = await ozura.cardSale({ ... });
|
|
919
|
+
await ozura.revokeWaxKey(waxKey); // key is spent; close the window immediately
|
|
769
920
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
921
|
+
// 2. User cancels checkout
|
|
922
|
+
router.post('/api/cancel', async (req) => {
|
|
923
|
+
const { waxKey } = await db.session.get(req.sessionId);
|
|
924
|
+
await ozura.revokeWaxKey(waxKey);
|
|
925
|
+
return Response.json({ ok: true });
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
// 3. Page/tab close (best-effort — browser may not deliver this)
|
|
929
|
+
// Use sendBeacon so the request survives navigation / tab close.
|
|
930
|
+
window.addEventListener('visibilitychange', () => {
|
|
931
|
+
if (document.visibilityState === 'hidden') {
|
|
932
|
+
navigator.sendBeacon('/api/cancel', JSON.stringify({ sessionId }));
|
|
933
|
+
}
|
|
934
|
+
});
|
|
773
935
|
|
|
774
|
-
|
|
936
|
+
// List transactions
|
|
937
|
+
const { transactions, pagination } = await ozura.listTransactions({
|
|
938
|
+
dateFrom: '2025-01-01',
|
|
939
|
+
dateTo: '2025-12-31',
|
|
940
|
+
transactionType: 'CreditCardSale',
|
|
941
|
+
page: 1,
|
|
942
|
+
limit: 50,
|
|
943
|
+
});
|
|
944
|
+
```
|
|
775
945
|
|
|
776
|
-
|
|
946
|
+
**`OzuraError`** (thrown by all `Ozura` methods):
|
|
777
947
|
|
|
778
948
|
```ts
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
billingLastName: billing.lastName,
|
|
790
|
-
billingEmail: billing.email!,
|
|
791
|
-
billingPhone: billing.phone!,
|
|
792
|
-
billingAddress1: billing.address!.line1,
|
|
793
|
-
billingCity: billing.address!.city,
|
|
794
|
-
billingState: billing.address!.state,
|
|
795
|
-
billingZipcode: billing.address!.zip,
|
|
796
|
-
billingCountry: billing.address!.country,
|
|
797
|
-
clientIpAddress: ip,
|
|
798
|
-
salesTaxExempt: false,
|
|
799
|
-
surchargePercent: '0.00',
|
|
800
|
-
tipAmount: '0.00',
|
|
801
|
-
};
|
|
949
|
+
try {
|
|
950
|
+
await ozura.cardSale(input);
|
|
951
|
+
} catch (err) {
|
|
952
|
+
if (err instanceof OzuraError) {
|
|
953
|
+
err.statusCode; // HTTP status code
|
|
954
|
+
err.message; // Normalized message
|
|
955
|
+
err.raw; // Raw API response string
|
|
956
|
+
err.retryAfter; // Seconds (only present on 429)
|
|
957
|
+
}
|
|
958
|
+
}
|
|
802
959
|
```
|
|
803
960
|
|
|
961
|
+
Rate limits: `cardSale` — 100 req/min per merchant. `listTransactions` — 200 req/min per merchant.
|
|
962
|
+
|
|
963
|
+
> **Retry behavior:** `mintWaxKey` and `listTransactions` retry on 5xx and network errors using exponential backoff (1 s / 2 s / 4 s…) up to `retries + 1` total attempts. **`cardSale` is never retried** — it is a non-idempotent financial operation and the result of a duplicate charge cannot be predicted.
|
|
964
|
+
|
|
804
965
|
---
|
|
805
966
|
|
|
806
|
-
|
|
967
|
+
### Route handler factories
|
|
807
968
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
969
|
+
The server package exports four factory functions covering two runtimes × two endpoints:
|
|
970
|
+
|
|
971
|
+
| Function | Runtime | Endpoint |
|
|
972
|
+
|---|---|---|
|
|
973
|
+
| `createMintWaxHandler` | Fetch API (Next.js App Router, Cloudflare, Vercel Edge) | `POST /api/mint-wax` |
|
|
974
|
+
| `createMintWaxMiddleware` | Express / Connect | `POST /api/mint-wax` |
|
|
975
|
+
| `createCardSaleHandler` | Fetch API | `POST /api/charge` |
|
|
976
|
+
| `createCardSaleMiddleware` | Express / Connect | `POST /api/charge` |
|
|
977
|
+
|
|
978
|
+
`createCardSaleHandler` / `createCardSaleMiddleware` accept a `CardSaleHandlerOptions` object:
|
|
979
|
+
|
|
980
|
+
```ts
|
|
981
|
+
interface CardSaleHandlerOptions {
|
|
982
|
+
/**
|
|
983
|
+
* Required. Return the charge amount as a decimal string.
|
|
984
|
+
* Never trust the amount from the request body — resolve it from your database.
|
|
985
|
+
*/
|
|
986
|
+
getAmount: (body: Record<string, unknown>) => Promise<string>;
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* Optional. Return the ISO 4217 currency code. Default: "USD".
|
|
990
|
+
*/
|
|
991
|
+
getCurrency?: (body: Record<string, unknown>) => Promise<string>;
|
|
992
|
+
}
|
|
812
993
|
```
|
|
813
994
|
|
|
814
|
-
|
|
995
|
+
Both handler factories validate `Content-Type: application/json`, run `validateBilling()`, extract the client IP from standard proxy headers, and return normalized `{ transactionId, amount, cardLastFour, cardBrand }` on success.
|
|
815
996
|
|
|
816
997
|
---
|
|
817
998
|
|
|
818
|
-
##
|
|
999
|
+
## Local development
|
|
1000
|
+
|
|
1001
|
+
The repository includes a development server at `dev-server.mjs` that serves the built frame assets and proxies vault API requests:
|
|
819
1002
|
|
|
820
1003
|
```bash
|
|
821
|
-
npm
|
|
822
|
-
npm run dev # build + serve demo at http://localhost:4242/demo/index.html
|
|
823
|
-
npm run watch # rebuild on save
|
|
824
|
-
npm run build # production build
|
|
1004
|
+
npm run dev # build + start dev server on http://localhost:4242
|
|
825
1005
|
```
|
|
826
1006
|
|
|
827
|
-
|
|
1007
|
+
Set `frameBaseUrl` to point your vault at the local server:
|
|
828
1008
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
| `react/index.cjs.js` | CJS | `require('@ozura/elements/react')` |
|
|
837
|
-
| `server/index.*.js` | ESM + CJS | `import { Ozura, createMintWaxHandler } from '@ozura/elements/server'` |
|
|
1009
|
+
```ts
|
|
1010
|
+
const vault = await OzVault.create({
|
|
1011
|
+
pubKey: 'pk_test_...',
|
|
1012
|
+
fetchWaxKey: createFetchWaxKey('/api/mint-wax'),
|
|
1013
|
+
frameBaseUrl: 'http://localhost:4242', // local dev only
|
|
1014
|
+
});
|
|
1015
|
+
```
|
|
838
1016
|
|
|
839
|
-
|
|
1017
|
+
Or in React:
|
|
1018
|
+
|
|
1019
|
+
```tsx
|
|
1020
|
+
<OzElements
|
|
1021
|
+
pubKey="pk_test_..."
|
|
1022
|
+
fetchWaxKey={createFetchWaxKey('/api/mint-wax')}
|
|
1023
|
+
frameBaseUrl="http://localhost:4242"
|
|
1024
|
+
>
|
|
1025
|
+
```
|
|
1026
|
+
|
|
1027
|
+
Configure environment variables for the dev server:
|
|
1028
|
+
|
|
1029
|
+
```bash
|
|
1030
|
+
VAULT_URL=https://vault-staging.example.com
|
|
1031
|
+
VAULT_API_KEY=vk_test_...
|
|
1032
|
+
```
|
|
840
1033
|
|
|
841
1034
|
---
|
|
842
1035
|
|
|
843
|
-
##
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
- [x] Unit test suite (Vitest + jsdom) — 168 tests, 0 failures
|
|
864
|
-
- [ ] Published to npm as `@ozura/elements`
|
|
865
|
-
- [ ] CDN delivery for UMD build (optional for v1)
|
|
866
|
-
|
|
867
|
-
### v0.3 — React ✅
|
|
868
|
-
- [x] `<OzElements>` provider — creates and owns an `OzVault` instance
|
|
869
|
-
- [x] `<OzCardNumber />`, `<OzExpiry />`, `<OzCvv />` field components
|
|
870
|
-
- [x] `useOzElements()` hook — `createToken` + `ready` flag
|
|
871
|
-
- [x] ESM + CJS React bundle, exported at `@ozura/elements/react`
|
|
872
|
-
- [ ] Published as a standalone `@oz/react-elements` package (optional; subpath export is sufficient for v1)
|
|
873
|
-
|
|
874
|
-
### v1.0 — Production ready
|
|
875
|
-
- [x] Production iframe hosting at `elements.ozura.com`
|
|
876
|
-
- [x] postMessage security covered (unit + E2E tests; see `docs/AUDIT.md`)
|
|
877
|
-
- [x] Security audit (consolidated in `docs/AUDIT.md`; all findings resolved or closed)
|
|
878
|
-
- [ ] Publish `@ozura/elements@1.0.0` to npm
|
|
879
|
-
- [ ] Public documentation site (or link to ozura-documentation)
|
|
880
|
-
- [ ] ACH / bank account element (pending Pay API support) — post-v1
|
|
881
|
-
- [ ] Saved payment method support (returning customers) — post-v1
|
|
1036
|
+
## Content Security Policy
|
|
1037
|
+
|
|
1038
|
+
The SDK loads iframes from the Ozura frame origin. Add the following directives to your CSP:
|
|
1039
|
+
|
|
1040
|
+
```
|
|
1041
|
+
frame-src https://elements.ozura.com;
|
|
1042
|
+
```
|
|
1043
|
+
|
|
1044
|
+
If loading custom fonts via `fonts[].cssSrc`, also allow the font stylesheet origin:
|
|
1045
|
+
|
|
1046
|
+
```
|
|
1047
|
+
style-src https://fonts.googleapis.com;
|
|
1048
|
+
font-src https://fonts.gstatic.com;
|
|
1049
|
+
```
|
|
1050
|
+
|
|
1051
|
+
To verify your CSP after a build:
|
|
1052
|
+
|
|
1053
|
+
```bash
|
|
1054
|
+
npm run check:csp
|
|
1055
|
+
```
|
|
882
1056
|
|
|
883
1057
|
---
|
|
884
1058
|
|
|
885
|
-
##
|
|
1059
|
+
## TypeScript reference
|
|
886
1060
|
|
|
887
|
-
|
|
1061
|
+
All public types are exported from `@ozura/elements`:
|
|
1062
|
+
|
|
1063
|
+
```ts
|
|
1064
|
+
import type {
|
|
1065
|
+
// Element types
|
|
1066
|
+
ElementType, // 'cardNumber' | 'cvv' | 'expirationDate'
|
|
1067
|
+
BankElementType, // 'accountNumber' | 'routingNumber'
|
|
1068
|
+
ElementOptions,
|
|
1069
|
+
ElementStyleConfig,
|
|
1070
|
+
ElementStyle,
|
|
1071
|
+
ElementChangeEvent,
|
|
1072
|
+
|
|
1073
|
+
// Vault config
|
|
1074
|
+
VaultOptions,
|
|
1075
|
+
FontSource,
|
|
1076
|
+
CssFontSource,
|
|
1077
|
+
CustomFontSource,
|
|
1078
|
+
Appearance,
|
|
1079
|
+
AppearanceVariables,
|
|
1080
|
+
OzTheme, // 'default' | 'night' | 'flat'
|
|
1081
|
+
|
|
1082
|
+
// Tokenization
|
|
1083
|
+
TokenizeOptions,
|
|
1084
|
+
BankTokenizeOptions,
|
|
1085
|
+
TokenResponse,
|
|
1086
|
+
BankTokenResponse,
|
|
1087
|
+
CardMetadata,
|
|
1088
|
+
BankAccountMetadata,
|
|
1089
|
+
|
|
1090
|
+
// Billing
|
|
1091
|
+
BillingDetails,
|
|
1092
|
+
BillingAddress,
|
|
1093
|
+
|
|
1094
|
+
// Card sale
|
|
1095
|
+
CardSaleRequest,
|
|
1096
|
+
CardSaleResponseData,
|
|
1097
|
+
CardSaleApiResponse,
|
|
1098
|
+
|
|
1099
|
+
// Transactions
|
|
1100
|
+
TransactionQueryParams,
|
|
1101
|
+
TransactionQueryPagination,
|
|
1102
|
+
TransactionQueryResponse,
|
|
1103
|
+
TransactionType,
|
|
1104
|
+
TransactionData,
|
|
1105
|
+
CardTransactionData,
|
|
1106
|
+
AchTransactionData,
|
|
1107
|
+
CryptoTransactionData,
|
|
1108
|
+
|
|
1109
|
+
// Errors
|
|
1110
|
+
OzErrorCode,
|
|
1111
|
+
} from '@ozura/elements';
|
|
1112
|
+
```
|
|
1113
|
+
|
|
1114
|
+
Server-specific types are exported from `@ozura/elements/server`:
|
|
1115
|
+
|
|
1116
|
+
```ts
|
|
1117
|
+
import type {
|
|
1118
|
+
OzuraConfig,
|
|
1119
|
+
CardSaleInput,
|
|
1120
|
+
MintWaxKeyOptions,
|
|
1121
|
+
MintWaxKeyResult,
|
|
1122
|
+
ListTransactionsInput,
|
|
1123
|
+
} from '@ozura/elements/server';
|
|
1124
|
+
```
|
|
1125
|
+
|
|
1126
|
+
React-specific types are exported from `@ozura/elements/react`:
|
|
1127
|
+
|
|
1128
|
+
```ts
|
|
1129
|
+
import type { OzFieldProps, OzCardProps, OzCardState, OzBankCardProps, OzBankCardState } from '@ozura/elements/react';
|
|
1130
|
+
```
|