@autra-io/react-native-card 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 +52 -0
- package/LICENSE +21 -0
- package/README.md +298 -0
- package/README.pt-BR.md +133 -0
- package/SECURITY.md +61 -0
- package/dist/index.cjs +341 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +53 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.js +334 -0
- package/dist/index.js.map +1 -0
- package/package.json +90 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var React = require('react');
|
|
4
|
+
var reactNativeWebview = require('react-native-webview');
|
|
5
|
+
|
|
6
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
7
|
+
|
|
8
|
+
var React__default = /*#__PURE__*/_interopDefault(React);
|
|
9
|
+
|
|
10
|
+
// src/AutraCardForm.ts
|
|
11
|
+
|
|
12
|
+
// src/messages.ts
|
|
13
|
+
var errorCodes = /* @__PURE__ */ new Set([
|
|
14
|
+
"configuration_error",
|
|
15
|
+
"token_expired",
|
|
16
|
+
"message_origin_rejected",
|
|
17
|
+
"message_invalid",
|
|
18
|
+
"validation_error",
|
|
19
|
+
"tokenization_failed"
|
|
20
|
+
]);
|
|
21
|
+
var originRejectedError = {
|
|
22
|
+
code: "message_origin_rejected",
|
|
23
|
+
message: "Rejected hosted card message origin.",
|
|
24
|
+
retryable: false
|
|
25
|
+
};
|
|
26
|
+
var invalidMessageError = {
|
|
27
|
+
code: "message_invalid",
|
|
28
|
+
message: "Invalid hosted card message.",
|
|
29
|
+
retryable: false
|
|
30
|
+
};
|
|
31
|
+
function deriveAllowedOrigins(cardBaseUrl) {
|
|
32
|
+
return [new URL(cardBaseUrl).origin];
|
|
33
|
+
}
|
|
34
|
+
function normalizeAllowedOrigins(origins) {
|
|
35
|
+
const normalized = [];
|
|
36
|
+
for (const candidate of origins) {
|
|
37
|
+
let url;
|
|
38
|
+
try {
|
|
39
|
+
url = new URL(candidate);
|
|
40
|
+
} catch {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (url.protocol !== "https:") {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (!normalized.includes(url.origin)) {
|
|
47
|
+
normalized.push(url.origin);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return normalized;
|
|
51
|
+
}
|
|
52
|
+
function parseAutraCardWebViewMessage(event, allowedOrigins) {
|
|
53
|
+
const eventOrigin = toOrigin(event.url);
|
|
54
|
+
if (eventOrigin === void 0 || !allowedOrigins.includes(eventOrigin)) {
|
|
55
|
+
return { ok: false, error: originRejectedError };
|
|
56
|
+
}
|
|
57
|
+
const rawMessage = parseJson(event.data);
|
|
58
|
+
if (!isRecord(rawMessage)) {
|
|
59
|
+
return { ok: false, error: invalidMessageError };
|
|
60
|
+
}
|
|
61
|
+
if (rawMessage.type === "autra.card.tokenized" && isRecord(rawMessage.payload)) {
|
|
62
|
+
const result = toTokenizedResult(rawMessage.payload);
|
|
63
|
+
if (result !== void 0) {
|
|
64
|
+
return { ok: true, message: { type: "tokenized", result } };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (rawMessage.type === "autra.card.error" && isRecord(rawMessage.error)) {
|
|
68
|
+
const error = toAutraCardError(rawMessage.error);
|
|
69
|
+
if (error !== void 0) {
|
|
70
|
+
return { ok: true, message: { type: "error", error } };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (rawMessage.type === "autra.card.ready") {
|
|
74
|
+
return {
|
|
75
|
+
ok: true,
|
|
76
|
+
message: {
|
|
77
|
+
type: "ready",
|
|
78
|
+
requestId: optionalString(rawMessage.requestId)
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return { ok: false, error: invalidMessageError };
|
|
83
|
+
}
|
|
84
|
+
function toOrigin(url) {
|
|
85
|
+
if (url === void 0) {
|
|
86
|
+
return void 0;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
return new URL(url).origin;
|
|
90
|
+
} catch {
|
|
91
|
+
return void 0;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function parseJson(data) {
|
|
95
|
+
try {
|
|
96
|
+
return JSON.parse(data);
|
|
97
|
+
} catch {
|
|
98
|
+
return void 0;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function isRecord(value) {
|
|
102
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
103
|
+
}
|
|
104
|
+
function optionalString(value) {
|
|
105
|
+
return typeof value === "string" ? value : void 0;
|
|
106
|
+
}
|
|
107
|
+
function toTokenizedResult(payload) {
|
|
108
|
+
const storedCard = isRecord(payload.storedCard) ? payload.storedCard : void 0;
|
|
109
|
+
const tokenizedCard = isRecord(payload.tokenizedCard) ? payload.tokenizedCard : void 0;
|
|
110
|
+
const slugStoredCard = optionalString(payload.slugStoredCard) ?? optionalString(storedCard?.slugStoredCard);
|
|
111
|
+
const slugToken = optionalString(payload.slugToken) ?? optionalString(tokenizedCard?.slugToken);
|
|
112
|
+
const tokenExpirationDate = optionalString(payload.tokenExpirationDate) ?? optionalString(tokenizedCard?.tokenExpirationDate);
|
|
113
|
+
if (slugStoredCard === void 0 && slugToken === void 0) {
|
|
114
|
+
return void 0;
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
slugStoredCard,
|
|
118
|
+
slugToken,
|
|
119
|
+
tokenExpirationDate,
|
|
120
|
+
brand: optionalString(payload.brand),
|
|
121
|
+
last4: optionalString(payload.last4),
|
|
122
|
+
expirationMonth: optionalString(payload.expirationMonth),
|
|
123
|
+
expirationYear: optionalString(payload.expirationYear),
|
|
124
|
+
requestId: optionalString(payload.requestId)
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function toAutraCardError(error) {
|
|
128
|
+
if (typeof error.code !== "string" || !isAutraCardErrorCode(error.code)) {
|
|
129
|
+
return void 0;
|
|
130
|
+
}
|
|
131
|
+
if (typeof error.message !== "string") {
|
|
132
|
+
return void 0;
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
code: error.code,
|
|
136
|
+
message: error.message,
|
|
137
|
+
requestId: optionalString(error.requestId),
|
|
138
|
+
retryable: typeof error.retryable === "boolean" ? error.retryable : void 0
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function isAutraCardErrorCode(code) {
|
|
142
|
+
return errorCodes.has(code);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/AutraCardForm.ts
|
|
146
|
+
var hostedPageLoadError = {
|
|
147
|
+
code: "tokenization_failed",
|
|
148
|
+
message: "Hosted card page failed to load.",
|
|
149
|
+
retryable: true
|
|
150
|
+
};
|
|
151
|
+
var hostedPageHttpError = {
|
|
152
|
+
code: "tokenization_failed",
|
|
153
|
+
message: "Hosted card page returned an HTTP error.",
|
|
154
|
+
retryable: true
|
|
155
|
+
};
|
|
156
|
+
var hostedSessionUnauthorizedError = {
|
|
157
|
+
code: "token_expired",
|
|
158
|
+
message: "Hosted card session expired or is not authorized.",
|
|
159
|
+
retryable: false
|
|
160
|
+
};
|
|
161
|
+
var invalidHostedUrlError = {
|
|
162
|
+
code: "configuration_error",
|
|
163
|
+
message: "Invalid hosted card URL.",
|
|
164
|
+
retryable: false
|
|
165
|
+
};
|
|
166
|
+
var insecureHostedUrlError = {
|
|
167
|
+
code: "configuration_error",
|
|
168
|
+
message: "Hosted card URL must use HTTPS.",
|
|
169
|
+
retryable: false
|
|
170
|
+
};
|
|
171
|
+
var invalidAllowedOriginsError = {
|
|
172
|
+
code: "configuration_error",
|
|
173
|
+
message: "allowedOrigins must contain at least one valid HTTPS origin.",
|
|
174
|
+
retryable: false
|
|
175
|
+
};
|
|
176
|
+
var blockedNavigationError = {
|
|
177
|
+
code: "message_origin_rejected",
|
|
178
|
+
message: "Blocked hosted card navigation.",
|
|
179
|
+
retryable: false
|
|
180
|
+
};
|
|
181
|
+
function buildAutraCardTokenizeUrl(hostedCardUrl) {
|
|
182
|
+
return hostedCardUrl;
|
|
183
|
+
}
|
|
184
|
+
function AutraCardForm(props) {
|
|
185
|
+
const allowedOriginsKey = props.allowedOrigins?.join(" ");
|
|
186
|
+
const configuration = React__default.default.useMemo(
|
|
187
|
+
() => resolveHostedCardConfiguration(props.hostedCardUrl, props.allowedOrigins),
|
|
188
|
+
// deps intentionally keyed by content (allowedOriginsKey), not array identity
|
|
189
|
+
[props.hostedCardUrl, allowedOriginsKey]
|
|
190
|
+
);
|
|
191
|
+
const terminalRef = React__default.default.useRef(false);
|
|
192
|
+
const onErrorRef = React__default.default.useRef(props.onError);
|
|
193
|
+
React__default.default.useEffect(() => {
|
|
194
|
+
onErrorRef.current = props.onError;
|
|
195
|
+
});
|
|
196
|
+
React__default.default.useEffect(() => {
|
|
197
|
+
if (!configuration.ok) {
|
|
198
|
+
onErrorRef.current(configuration.error);
|
|
199
|
+
}
|
|
200
|
+
}, [configuration]);
|
|
201
|
+
if (!configuration.ok) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
const { tokenizeUrl, allowedOrigins } = configuration;
|
|
205
|
+
const handleMessage = (event) => {
|
|
206
|
+
if (terminalRef.current) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const result = parseAutraCardWebViewMessage(event.nativeEvent, allowedOrigins);
|
|
210
|
+
if (!result.ok) {
|
|
211
|
+
props.onError(result.error);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (result.message.type === "tokenized") {
|
|
215
|
+
terminalRef.current = true;
|
|
216
|
+
props.onSuccess(result.message.result);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (result.message.type === "error") {
|
|
220
|
+
props.onError(result.message.error);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
props.onReady?.(result.message.requestId);
|
|
224
|
+
};
|
|
225
|
+
const handleHttpError = (event) => {
|
|
226
|
+
if (terminalRef.current) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (!isHostedDocumentUrl(event.nativeEvent.url, tokenizeUrl)) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (event.nativeEvent.statusCode === 401 || event.nativeEvent.statusCode === 403) {
|
|
233
|
+
props.onError(hostedSessionUnauthorizedError);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
props.onError(hostedPageHttpError);
|
|
237
|
+
};
|
|
238
|
+
const handleShouldStartLoadWithRequest = (request) => {
|
|
239
|
+
if (isAllowedHostedNavigation(request.url, allowedOrigins)) {
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
if (!terminalRef.current) {
|
|
243
|
+
props.onError(blockedNavigationError);
|
|
244
|
+
}
|
|
245
|
+
return false;
|
|
246
|
+
};
|
|
247
|
+
const WebViewComponent = reactNativeWebview.WebView;
|
|
248
|
+
return React__default.default.createElement(WebViewComponent, {
|
|
249
|
+
source: {
|
|
250
|
+
uri: tokenizeUrl
|
|
251
|
+
},
|
|
252
|
+
// Security: react-native-webview hands URLs that fail originWhitelist to
|
|
253
|
+
// the OS (Linking.openURL) BEFORE onShouldStartLoadWithRequest runs — a
|
|
254
|
+
// compromised hosted page could silently bounce the user to an external
|
|
255
|
+
// browser (phishing surface). The payment-WebView hardening pattern is a
|
|
256
|
+
// wildcard whitelist plus a full allowlist in
|
|
257
|
+
// onShouldStartLoadWithRequest: returning false there blocks the
|
|
258
|
+
// navigation WITHOUT opening it externally.
|
|
259
|
+
originWhitelist: ["*"],
|
|
260
|
+
onMessage: handleMessage,
|
|
261
|
+
onError: () => {
|
|
262
|
+
if (!terminalRef.current) {
|
|
263
|
+
props.onError(hostedPageLoadError);
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
onHttpError: handleHttpError,
|
|
267
|
+
onShouldStartLoadWithRequest: handleShouldStartLoadWithRequest,
|
|
268
|
+
mixedContentMode: "never",
|
|
269
|
+
allowFileAccess: false,
|
|
270
|
+
allowsBackForwardNavigationGestures: false,
|
|
271
|
+
// PCI hygiene: never persist the hosted card page (or anything typed in
|
|
272
|
+
// it) to disk caches.
|
|
273
|
+
incognito: true,
|
|
274
|
+
cacheEnabled: false,
|
|
275
|
+
style: props.style,
|
|
276
|
+
testID: props.testID
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
function resolveHostedCardConfiguration(hostedCardUrl, allowedOriginsInput) {
|
|
280
|
+
const parsedHostedCardUrl = parseHostedCardUrl(hostedCardUrl);
|
|
281
|
+
if (!parsedHostedCardUrl.ok) {
|
|
282
|
+
return parsedHostedCardUrl;
|
|
283
|
+
}
|
|
284
|
+
const allowedOrigins = normalizeAllowedOrigins(
|
|
285
|
+
allowedOriginsInput ?? deriveAllowedOrigins(hostedCardUrl)
|
|
286
|
+
);
|
|
287
|
+
if (allowedOrigins.length === 0) {
|
|
288
|
+
return { ok: false, error: invalidAllowedOriginsError };
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
ok: true,
|
|
292
|
+
tokenizeUrl: buildAutraCardTokenizeUrl(hostedCardUrl),
|
|
293
|
+
allowedOrigins
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function parseHostedCardUrl(hostedCardUrl) {
|
|
297
|
+
let url;
|
|
298
|
+
try {
|
|
299
|
+
url = new URL(hostedCardUrl);
|
|
300
|
+
} catch {
|
|
301
|
+
return { ok: false, error: invalidHostedUrlError };
|
|
302
|
+
}
|
|
303
|
+
if (url.protocol !== "https:") {
|
|
304
|
+
return { ok: false, error: insecureHostedUrlError };
|
|
305
|
+
}
|
|
306
|
+
return { ok: true, url };
|
|
307
|
+
}
|
|
308
|
+
function isAllowedHostedNavigation(navigationUrl, allowedOrigins) {
|
|
309
|
+
let url;
|
|
310
|
+
try {
|
|
311
|
+
url = new URL(navigationUrl);
|
|
312
|
+
} catch {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
if (url.protocol !== "https:") {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
return allowedOrigins.includes(url.origin);
|
|
319
|
+
}
|
|
320
|
+
function isHostedDocumentUrl(eventUrl, documentUrl) {
|
|
321
|
+
if (eventUrl === void 0) {
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
let failed;
|
|
325
|
+
let document;
|
|
326
|
+
try {
|
|
327
|
+
failed = new URL(eventUrl);
|
|
328
|
+
document = new URL(documentUrl);
|
|
329
|
+
} catch {
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
return failed.origin === document.origin && failed.pathname === document.pathname;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// src/index.ts
|
|
336
|
+
var AUTRA_REACT_NATIVE_CARD_SDK_VERSION = "0.1.0";
|
|
337
|
+
|
|
338
|
+
exports.AUTRA_REACT_NATIVE_CARD_SDK_VERSION = AUTRA_REACT_NATIVE_CARD_SDK_VERSION;
|
|
339
|
+
exports.AutraCardForm = AutraCardForm;
|
|
340
|
+
//# sourceMappingURL=index.cjs.map
|
|
341
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/messages.ts","../src/AutraCardForm.ts","../src/index.ts"],"names":["React","WebView"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { AutraCardError, AutraCardErrorCode, AutraCardTokenizedResult } from './index';\n\nexport type AutraCardWebViewNativeMessage = {\n data: string;\n url?: string;\n};\n\nexport type AutraCardHostedMessage =\n | { type: 'tokenized'; result: AutraCardTokenizedResult }\n | { type: 'error'; error: AutraCardError }\n | { type: 'ready'; requestId?: string };\n\nexport type ParseAutraCardWebViewMessageResult =\n { ok: true; message: AutraCardHostedMessage } | { ok: false; error: AutraCardError };\n\nconst errorCodes = new Set<AutraCardErrorCode>([\n 'configuration_error',\n 'token_expired',\n 'message_origin_rejected',\n 'message_invalid',\n 'validation_error',\n 'tokenization_failed',\n]);\n\nconst originRejectedError: AutraCardError = {\n code: 'message_origin_rejected',\n message: 'Rejected hosted card message origin.',\n retryable: false,\n};\n\nconst invalidMessageError: AutraCardError = {\n code: 'message_invalid',\n message: 'Invalid hosted card message.',\n retryable: false,\n};\n\nexport function deriveAllowedOrigins(cardBaseUrl: string): string[] {\n return [new URL(cardBaseUrl).origin];\n}\n\n/**\n * Normalizes a caller-supplied origin list into canonical HTTPS origins.\n *\n * Guards against silent lockouts: `['https://cards.example/']` (trailing\n * slash) never equals a browser-reported origin and would reject every\n * hosted message. Entries that are not valid HTTPS URLs are dropped —\n * postMessage trust must never be extended to plaintext origins.\n */\nexport function normalizeAllowedOrigins(origins: readonly string[]): string[] {\n const normalized: string[] = [];\n\n for (const candidate of origins) {\n let url: URL;\n\n try {\n url = new URL(candidate);\n } catch {\n continue;\n }\n\n if (url.protocol !== 'https:') {\n continue;\n }\n\n if (!normalized.includes(url.origin)) {\n normalized.push(url.origin);\n }\n }\n\n return normalized;\n}\n\nexport function parseAutraCardWebViewMessage(\n event: AutraCardWebViewNativeMessage,\n allowedOrigins: readonly string[],\n): ParseAutraCardWebViewMessageResult {\n const eventOrigin = toOrigin(event.url);\n\n if (eventOrigin === undefined || !allowedOrigins.includes(eventOrigin)) {\n return { ok: false, error: originRejectedError };\n }\n\n const rawMessage = parseJson(event.data);\n\n if (!isRecord(rawMessage)) {\n return { ok: false, error: invalidMessageError };\n }\n\n if (rawMessage.type === 'autra.card.tokenized' && isRecord(rawMessage.payload)) {\n const result = toTokenizedResult(rawMessage.payload);\n\n if (result !== undefined) {\n return { ok: true, message: { type: 'tokenized', result } };\n }\n }\n\n if (rawMessage.type === 'autra.card.error' && isRecord(rawMessage.error)) {\n const error = toAutraCardError(rawMessage.error);\n\n if (error !== undefined) {\n return { ok: true, message: { type: 'error', error } };\n }\n }\n\n if (rawMessage.type === 'autra.card.ready') {\n return {\n ok: true,\n message: {\n type: 'ready',\n requestId: optionalString(rawMessage.requestId),\n },\n };\n }\n\n return { ok: false, error: invalidMessageError };\n}\n\nfunction toOrigin(url: string | undefined): string | undefined {\n if (url === undefined) {\n return undefined;\n }\n\n try {\n return new URL(url).origin;\n } catch {\n return undefined;\n }\n}\n\nfunction parseJson(data: string): unknown {\n try {\n return JSON.parse(data) as unknown;\n } catch {\n return undefined;\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction optionalString(value: unknown): string | undefined {\n return typeof value === 'string' ? value : undefined;\n}\n\nfunction toTokenizedResult(payload: Record<string, unknown>): AutraCardTokenizedResult | undefined {\n const storedCard = isRecord(payload.storedCard) ? payload.storedCard : undefined;\n const tokenizedCard = isRecord(payload.tokenizedCard) ? payload.tokenizedCard : undefined;\n\n const slugStoredCard =\n optionalString(payload.slugStoredCard) ?? optionalString(storedCard?.slugStoredCard);\n const slugToken = optionalString(payload.slugToken) ?? optionalString(tokenizedCard?.slugToken);\n const tokenExpirationDate =\n optionalString(payload.tokenExpirationDate) ??\n optionalString(tokenizedCard?.tokenExpirationDate);\n\n if (slugStoredCard === undefined && slugToken === undefined) {\n return undefined;\n }\n\n return {\n slugStoredCard,\n slugToken,\n tokenExpirationDate,\n brand: optionalString(payload.brand),\n last4: optionalString(payload.last4),\n expirationMonth: optionalString(payload.expirationMonth),\n expirationYear: optionalString(payload.expirationYear),\n requestId: optionalString(payload.requestId),\n };\n}\n\nfunction toAutraCardError(error: Record<string, unknown>): AutraCardError | undefined {\n if (typeof error.code !== 'string' || !isAutraCardErrorCode(error.code)) {\n return undefined;\n }\n\n if (typeof error.message !== 'string') {\n return undefined;\n }\n\n return {\n code: error.code,\n message: error.message,\n requestId: optionalString(error.requestId),\n retryable: typeof error.retryable === 'boolean' ? error.retryable : undefined,\n };\n}\n\nfunction isAutraCardErrorCode(code: string): code is AutraCardErrorCode {\n return errorCodes.has(code as AutraCardErrorCode);\n}\n","import React from 'react';\nimport type { StyleProp, ViewStyle } from 'react-native';\nimport { WebView } from 'react-native-webview';\n\nimport type { AutraCardError, AutraCardTokenizedResult } from './index';\nimport {\n deriveAllowedOrigins,\n normalizeAllowedOrigins,\n parseAutraCardWebViewMessage,\n type AutraCardWebViewNativeMessage,\n} from './messages';\n\nexport type AutraCardFormProps = {\n hostedCardUrl: string;\n onSuccess: (result: AutraCardTokenizedResult) => void;\n onError: (error: AutraCardError) => void;\n allowedOrigins?: readonly string[];\n onReady?: (requestId?: string) => void;\n style?: StyleProp<ViewStyle>;\n testID?: string;\n};\n\ntype WebViewMessageEvent = {\n nativeEvent: AutraCardWebViewNativeMessage;\n};\n\ntype WebViewHttpErrorEvent = {\n nativeEvent: {\n statusCode: number;\n url?: string;\n };\n};\n\ntype WebViewNavigationRequest = {\n url: string;\n};\n\ntype AutraCardWebViewProps = {\n source: {\n uri: string;\n };\n originWhitelist: string[];\n onMessage: (event: WebViewMessageEvent) => void;\n onError: () => void;\n onHttpError: (event: WebViewHttpErrorEvent) => void;\n onShouldStartLoadWithRequest: (request: WebViewNavigationRequest) => boolean;\n mixedContentMode: 'never';\n allowFileAccess: boolean;\n allowsBackForwardNavigationGestures: boolean;\n incognito: boolean;\n cacheEnabled: boolean;\n style?: StyleProp<ViewStyle>;\n testID?: string;\n};\n\nconst hostedPageLoadError: AutraCardError = {\n code: 'tokenization_failed',\n message: 'Hosted card page failed to load.',\n retryable: true,\n};\n\nconst hostedPageHttpError: AutraCardError = {\n code: 'tokenization_failed',\n message: 'Hosted card page returned an HTTP error.',\n retryable: true,\n};\n\nconst hostedSessionUnauthorizedError: AutraCardError = {\n code: 'token_expired',\n message: 'Hosted card session expired or is not authorized.',\n retryable: false,\n};\n\nconst invalidHostedUrlError: AutraCardError = {\n code: 'configuration_error',\n message: 'Invalid hosted card URL.',\n retryable: false,\n};\n\nconst insecureHostedUrlError: AutraCardError = {\n code: 'configuration_error',\n message: 'Hosted card URL must use HTTPS.',\n retryable: false,\n};\n\nconst invalidAllowedOriginsError: AutraCardError = {\n code: 'configuration_error',\n message: 'allowedOrigins must contain at least one valid HTTPS origin.',\n retryable: false,\n};\n\nconst blockedNavigationError: AutraCardError = {\n code: 'message_origin_rejected',\n message: 'Blocked hosted card navigation.',\n retryable: false,\n};\n\nexport function buildAutraCardTokenizeUrl(hostedCardUrl: string): string {\n return hostedCardUrl;\n}\n\ntype HostedCardConfiguration =\n | { ok: true; tokenizeUrl: string; allowedOrigins: readonly string[] }\n | { ok: false; error: AutraCardError };\n\nexport function AutraCardForm(props: AutraCardFormProps): React.ReactElement | null {\n // Key the memo by origin CONTENT so an inline `allowedOrigins={[...]}`\n // (new array identity every render) neither recomputes nor re-emits.\n const allowedOriginsKey = props.allowedOrigins?.join(' ');\n const configuration = React.useMemo<HostedCardConfiguration>(\n () => resolveHostedCardConfiguration(props.hostedCardUrl, props.allowedOrigins),\n // deps intentionally keyed by content (allowedOriginsKey), not array identity\n [props.hostedCardUrl, allowedOriginsKey],\n );\n\n // Terminal guard: once the hosted page reports a successful tokenization,\n // every later event (duplicate `tokenized`, stray errors, sub-frame noise)\n // is ignored so the consumer can never double-charge or double-navigate.\n const terminalRef = React.useRef(false);\n\n const onErrorRef = React.useRef(props.onError);\n React.useEffect(() => {\n onErrorRef.current = props.onError;\n });\n\n // Configuration errors are emitted from an effect: calling props.onError\n // during render would trigger a parent setState mid-render (React error\n // \"Cannot update a component while rendering a different component\") and\n // would re-fire on every re-render.\n React.useEffect(() => {\n if (!configuration.ok) {\n onErrorRef.current(configuration.error);\n }\n }, [configuration]);\n\n if (!configuration.ok) {\n return null;\n }\n\n const { tokenizeUrl, allowedOrigins } = configuration;\n\n const handleMessage = (event: WebViewMessageEvent): void => {\n if (terminalRef.current) {\n return;\n }\n\n const result = parseAutraCardWebViewMessage(event.nativeEvent, allowedOrigins);\n\n if (!result.ok) {\n props.onError(result.error);\n return;\n }\n\n if (result.message.type === 'tokenized') {\n terminalRef.current = true;\n props.onSuccess(result.message.result);\n return;\n }\n\n if (result.message.type === 'error') {\n props.onError(result.message.error);\n return;\n }\n\n props.onReady?.(result.message.requestId);\n };\n\n const handleHttpError = (event: WebViewHttpErrorEvent): void => {\n if (terminalRef.current) {\n return;\n }\n\n // On Android onHttpError also fires for sub-resources (favicon, assets,\n // page-initiated fetches). Only a failure of the hosted DOCUMENT itself\n // is fatal to the flow — the page reports its own API errors through the\n // postMessage channel.\n if (!isHostedDocumentUrl(event.nativeEvent.url, tokenizeUrl)) {\n return;\n }\n\n if (event.nativeEvent.statusCode === 401 || event.nativeEvent.statusCode === 403) {\n props.onError(hostedSessionUnauthorizedError);\n return;\n }\n\n props.onError(hostedPageHttpError);\n };\n\n const handleShouldStartLoadWithRequest = (request: WebViewNavigationRequest): boolean => {\n if (isAllowedHostedNavigation(request.url, allowedOrigins)) {\n return true;\n }\n\n if (!terminalRef.current) {\n props.onError(blockedNavigationError);\n }\n return false;\n };\n\n const WebViewComponent = WebView as unknown as React.ComponentType<AutraCardWebViewProps>;\n\n return React.createElement(WebViewComponent, {\n source: {\n uri: tokenizeUrl,\n },\n // Security: react-native-webview hands URLs that fail originWhitelist to\n // the OS (Linking.openURL) BEFORE onShouldStartLoadWithRequest runs — a\n // compromised hosted page could silently bounce the user to an external\n // browser (phishing surface). The payment-WebView hardening pattern is a\n // wildcard whitelist plus a full allowlist in\n // onShouldStartLoadWithRequest: returning false there blocks the\n // navigation WITHOUT opening it externally.\n originWhitelist: ['*'],\n onMessage: handleMessage,\n onError: () => {\n if (!terminalRef.current) {\n props.onError(hostedPageLoadError);\n }\n },\n onHttpError: handleHttpError,\n onShouldStartLoadWithRequest: handleShouldStartLoadWithRequest,\n mixedContentMode: 'never',\n allowFileAccess: false,\n allowsBackForwardNavigationGestures: false,\n // PCI hygiene: never persist the hosted card page (or anything typed in\n // it) to disk caches.\n incognito: true,\n cacheEnabled: false,\n style: props.style,\n testID: props.testID,\n });\n}\n\nfunction resolveHostedCardConfiguration(\n hostedCardUrl: string,\n allowedOriginsInput: readonly string[] | undefined,\n): HostedCardConfiguration {\n const parsedHostedCardUrl = parseHostedCardUrl(hostedCardUrl);\n\n if (!parsedHostedCardUrl.ok) {\n return parsedHostedCardUrl;\n }\n\n // Normalization guards against silent lockouts: an entry with a trailing\n // slash/path (`https://cards.example/`) would never equal a browser origin\n // and would reject every hosted message. Non-HTTPS entries are dropped.\n const allowedOrigins = normalizeAllowedOrigins(\n allowedOriginsInput ?? deriveAllowedOrigins(hostedCardUrl),\n );\n\n if (allowedOrigins.length === 0) {\n return { ok: false, error: invalidAllowedOriginsError };\n }\n\n return {\n ok: true,\n tokenizeUrl: buildAutraCardTokenizeUrl(hostedCardUrl),\n allowedOrigins,\n };\n}\n\nfunction parseHostedCardUrl(\n hostedCardUrl: string,\n): { ok: true; url: URL } | { ok: false; error: AutraCardError } {\n let url: URL;\n\n try {\n url = new URL(hostedCardUrl);\n } catch {\n return { ok: false, error: invalidHostedUrlError };\n }\n\n if (url.protocol !== 'https:') {\n return { ok: false, error: insecureHostedUrlError };\n }\n\n return { ok: true, url };\n}\n\nfunction isAllowedHostedNavigation(\n navigationUrl: string,\n allowedOrigins: readonly string[],\n): boolean {\n let url: URL;\n\n try {\n url = new URL(navigationUrl);\n } catch {\n return false;\n }\n\n if (url.protocol !== 'https:') {\n return false;\n }\n\n return allowedOrigins.includes(url.origin);\n}\n\nfunction isHostedDocumentUrl(eventUrl: string | undefined, documentUrl: string): boolean {\n if (eventUrl === undefined) {\n // Older react-native-webview versions omit the URL — stay conservative\n // and treat the failure as fatal rather than silently swallowing it.\n return true;\n }\n\n let failed: URL;\n let document: URL;\n\n try {\n failed = new URL(eventUrl);\n document = new URL(documentUrl);\n } catch {\n return true;\n }\n\n return failed.origin === document.origin && failed.pathname === document.pathname;\n}\n","/**\n * @autra-io/react-native-card — hosted card tokenization SDK exports.\n *\n * Phase: v0 hosted-WebView tokenization. The SDK owns only the React Native\n * component shell and message bridge; the hosted card page owns PAN/CVV entry.\n *\n * Security (payment-adjacent SDK):\n * - Never embed or log PAN, CVV, expiry or cardholder data.\n * - Never place OAuth clientSecret or a strong accessToken in the app.\n * - The mobile context must only ever load a short-lived hostedCardUrl.\n */\n\nexport { AutraCardForm } from './AutraCardForm';\nexport type { AutraCardFormProps } from './AutraCardForm';\n\n/** Semantic version marker for the v0 implementation phase. */\nexport const AUTRA_REACT_NATIVE_CARD_SDK_VERSION = '0.1.0';\n\n/**\n * Result returned by a successful card tokenization.\n * `slugStoredCard` is the opaque reference the backend uses to charge later.\n * Never populate this with raw PAN/CVV; only tokenized/masked references.\n */\nexport type AutraCardTokenizedResult = {\n slugStoredCard?: string;\n slugToken?: string;\n tokenExpirationDate?: string;\n brand?: string;\n last4?: string;\n expirationMonth?: string;\n expirationYear?: string;\n requestId?: string;\n};\n\n/** Normalized v0 error code taxonomy surfaced by the SDK. */\nexport type AutraCardErrorCode =\n | 'configuration_error'\n | 'token_expired'\n | 'message_origin_rejected'\n | 'message_invalid'\n | 'validation_error'\n | 'tokenization_failed';\n\nexport type AutraCardError = {\n code: AutraCardErrorCode;\n message: string;\n requestId?: string;\n retryable?: boolean;\n};\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
type AutraCardFormProps = {
|
|
5
|
+
hostedCardUrl: string;
|
|
6
|
+
onSuccess: (result: AutraCardTokenizedResult) => void;
|
|
7
|
+
onError: (error: AutraCardError) => void;
|
|
8
|
+
allowedOrigins?: readonly string[];
|
|
9
|
+
onReady?: (requestId?: string) => void;
|
|
10
|
+
style?: StyleProp<ViewStyle>;
|
|
11
|
+
testID?: string;
|
|
12
|
+
};
|
|
13
|
+
declare function AutraCardForm(props: AutraCardFormProps): React.ReactElement | null;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @autra-io/react-native-card — hosted card tokenization SDK exports.
|
|
17
|
+
*
|
|
18
|
+
* Phase: v0 hosted-WebView tokenization. The SDK owns only the React Native
|
|
19
|
+
* component shell and message bridge; the hosted card page owns PAN/CVV entry.
|
|
20
|
+
*
|
|
21
|
+
* Security (payment-adjacent SDK):
|
|
22
|
+
* - Never embed or log PAN, CVV, expiry or cardholder data.
|
|
23
|
+
* - Never place OAuth clientSecret or a strong accessToken in the app.
|
|
24
|
+
* - The mobile context must only ever load a short-lived hostedCardUrl.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** Semantic version marker for the v0 implementation phase. */
|
|
28
|
+
declare const AUTRA_REACT_NATIVE_CARD_SDK_VERSION = "0.1.0";
|
|
29
|
+
/**
|
|
30
|
+
* Result returned by a successful card tokenization.
|
|
31
|
+
* `slugStoredCard` is the opaque reference the backend uses to charge later.
|
|
32
|
+
* Never populate this with raw PAN/CVV; only tokenized/masked references.
|
|
33
|
+
*/
|
|
34
|
+
type AutraCardTokenizedResult = {
|
|
35
|
+
slugStoredCard?: string;
|
|
36
|
+
slugToken?: string;
|
|
37
|
+
tokenExpirationDate?: string;
|
|
38
|
+
brand?: string;
|
|
39
|
+
last4?: string;
|
|
40
|
+
expirationMonth?: string;
|
|
41
|
+
expirationYear?: string;
|
|
42
|
+
requestId?: string;
|
|
43
|
+
};
|
|
44
|
+
/** Normalized v0 error code taxonomy surfaced by the SDK. */
|
|
45
|
+
type AutraCardErrorCode = 'configuration_error' | 'token_expired' | 'message_origin_rejected' | 'message_invalid' | 'validation_error' | 'tokenization_failed';
|
|
46
|
+
type AutraCardError = {
|
|
47
|
+
code: AutraCardErrorCode;
|
|
48
|
+
message: string;
|
|
49
|
+
requestId?: string;
|
|
50
|
+
retryable?: boolean;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export { AUTRA_REACT_NATIVE_CARD_SDK_VERSION, type AutraCardError, type AutraCardErrorCode, AutraCardForm, type AutraCardFormProps, type AutraCardTokenizedResult };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
type AutraCardFormProps = {
|
|
5
|
+
hostedCardUrl: string;
|
|
6
|
+
onSuccess: (result: AutraCardTokenizedResult) => void;
|
|
7
|
+
onError: (error: AutraCardError) => void;
|
|
8
|
+
allowedOrigins?: readonly string[];
|
|
9
|
+
onReady?: (requestId?: string) => void;
|
|
10
|
+
style?: StyleProp<ViewStyle>;
|
|
11
|
+
testID?: string;
|
|
12
|
+
};
|
|
13
|
+
declare function AutraCardForm(props: AutraCardFormProps): React.ReactElement | null;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @autra-io/react-native-card — hosted card tokenization SDK exports.
|
|
17
|
+
*
|
|
18
|
+
* Phase: v0 hosted-WebView tokenization. The SDK owns only the React Native
|
|
19
|
+
* component shell and message bridge; the hosted card page owns PAN/CVV entry.
|
|
20
|
+
*
|
|
21
|
+
* Security (payment-adjacent SDK):
|
|
22
|
+
* - Never embed or log PAN, CVV, expiry or cardholder data.
|
|
23
|
+
* - Never place OAuth clientSecret or a strong accessToken in the app.
|
|
24
|
+
* - The mobile context must only ever load a short-lived hostedCardUrl.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** Semantic version marker for the v0 implementation phase. */
|
|
28
|
+
declare const AUTRA_REACT_NATIVE_CARD_SDK_VERSION = "0.1.0";
|
|
29
|
+
/**
|
|
30
|
+
* Result returned by a successful card tokenization.
|
|
31
|
+
* `slugStoredCard` is the opaque reference the backend uses to charge later.
|
|
32
|
+
* Never populate this with raw PAN/CVV; only tokenized/masked references.
|
|
33
|
+
*/
|
|
34
|
+
type AutraCardTokenizedResult = {
|
|
35
|
+
slugStoredCard?: string;
|
|
36
|
+
slugToken?: string;
|
|
37
|
+
tokenExpirationDate?: string;
|
|
38
|
+
brand?: string;
|
|
39
|
+
last4?: string;
|
|
40
|
+
expirationMonth?: string;
|
|
41
|
+
expirationYear?: string;
|
|
42
|
+
requestId?: string;
|
|
43
|
+
};
|
|
44
|
+
/** Normalized v0 error code taxonomy surfaced by the SDK. */
|
|
45
|
+
type AutraCardErrorCode = 'configuration_error' | 'token_expired' | 'message_origin_rejected' | 'message_invalid' | 'validation_error' | 'tokenization_failed';
|
|
46
|
+
type AutraCardError = {
|
|
47
|
+
code: AutraCardErrorCode;
|
|
48
|
+
message: string;
|
|
49
|
+
requestId?: string;
|
|
50
|
+
retryable?: boolean;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export { AUTRA_REACT_NATIVE_CARD_SDK_VERSION, type AutraCardError, type AutraCardErrorCode, AutraCardForm, type AutraCardFormProps, type AutraCardTokenizedResult };
|