@glideidentity/web-client-sdk 5.0.1 → 5.1.1-beta.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 +8 -108
- package/dist/adapters/angular/index.js +1 -0
- package/dist/adapters/angular/phone-auth.service.d.ts +18 -0
- package/dist/adapters/angular/phone-auth.service.js +26 -0
- package/dist/adapters/react/index.js +3 -0
- package/dist/adapters/react/useClient.js +1 -0
- package/dist/adapters/react/usePhoneAuth.js +16 -1
- package/dist/adapters/vanilla/client.js +1 -0
- package/dist/adapters/vanilla/index.js +1 -0
- package/dist/adapters/vanilla/phone-auth.js +31 -0
- package/dist/adapters/vue/index.js +4 -0
- package/dist/adapters/vue/useClient.js +5 -0
- package/dist/adapters/vue/usePhoneAuth.js +20 -1
- package/dist/browser/web-client-sdk.min.js +1 -2
- package/dist/browser.js +6 -0
- package/dist/core/client.js +12 -0
- package/dist/core/logger.js +81 -1
- package/dist/core/phone-auth/api-types.d.ts +1 -4
- package/dist/core/phone-auth/api-types.js +83 -0
- package/dist/core/phone-auth/client.js +374 -38
- package/dist/core/phone-auth/error-utils.js +83 -1
- package/dist/core/phone-auth/index.d.ts +1 -1
- package/dist/core/phone-auth/index.js +2 -2
- package/dist/core/phone-auth/status-types.d.ts +78 -0
- package/dist/core/phone-auth/status-types.js +17 -0
- package/dist/core/phone-auth/strategies/desktop.d.ts +2 -0
- package/dist/core/phone-auth/strategies/desktop.js +136 -13
- package/dist/core/phone-auth/strategies/index.d.ts +4 -0
- package/dist/core/phone-auth/strategies/index.js +4 -0
- package/dist/core/phone-auth/strategies/link.d.ts +2 -0
- package/dist/core/phone-auth/strategies/link.js +97 -13
- package/dist/core/phone-auth/strategies/ts43.d.ts +19 -0
- package/dist/core/phone-auth/strategies/ts43.js +33 -2
- package/dist/core/phone-auth/strategies/types.js +4 -0
- package/dist/core/phone-auth/type-guards.js +131 -0
- package/dist/core/phone-auth/types.d.ts +5 -0
- package/dist/core/phone-auth/types.js +32 -0
- package/dist/core/phone-auth/ui/mobile-debug-console.js +28 -2
- package/dist/core/phone-auth/ui/modal.d.ts +55 -33
- package/dist/core/phone-auth/ui/modal.js +422 -889
- package/dist/core/phone-auth/validation-utils.d.ts +0 -9
- package/dist/core/phone-auth/validation-utils.js +34 -25
- package/dist/core/version.js +2 -1
- package/dist/esm/adapters/angular/index.js +1 -0
- package/dist/esm/adapters/angular/phone-auth.service.d.ts +18 -0
- package/dist/esm/adapters/angular/phone-auth.service.js +26 -0
- package/dist/esm/adapters/react/index.js +3 -0
- package/dist/esm/adapters/react/useClient.js +1 -0
- package/dist/esm/adapters/react/usePhoneAuth.js +16 -1
- package/dist/esm/adapters/vanilla/client.js +1 -0
- package/dist/esm/adapters/vanilla/index.js +1 -0
- package/dist/esm/adapters/vanilla/phone-auth.d.ts +24 -0
- package/dist/esm/adapters/vanilla/phone-auth.js +31 -0
- package/dist/esm/adapters/vue/index.js +4 -0
- package/dist/esm/adapters/vue/useClient.js +5 -0
- package/dist/esm/adapters/vue/usePhoneAuth.js +20 -1
- package/dist/esm/browser.js +6 -0
- package/dist/esm/core/client.d.ts +10 -0
- package/dist/esm/core/client.js +12 -0
- package/dist/esm/core/logger.d.ts +53 -0
- package/dist/esm/core/logger.js +81 -1
- package/dist/esm/core/phone-auth/api-types.d.ts +313 -1
- package/dist/esm/core/phone-auth/api-types.js +83 -0
- package/dist/esm/core/phone-auth/client.d.ts +144 -0
- package/dist/esm/core/phone-auth/client.js +375 -39
- package/dist/esm/core/phone-auth/error-utils.d.ts +29 -0
- package/dist/esm/core/phone-auth/error-utils.js +83 -1
- package/dist/esm/core/phone-auth/index.d.ts +1 -1
- package/dist/esm/core/phone-auth/index.js +4 -2
- package/dist/esm/core/phone-auth/status-types.d.ts +78 -0
- package/dist/esm/core/phone-auth/status-types.js +17 -0
- package/dist/esm/core/phone-auth/strategies/desktop.d.ts +65 -0
- package/dist/esm/core/phone-auth/strategies/desktop.js +136 -13
- package/dist/esm/core/phone-auth/strategies/index.d.ts +4 -0
- package/dist/esm/core/phone-auth/strategies/index.js +4 -0
- package/dist/esm/core/phone-auth/strategies/link.d.ts +50 -0
- package/dist/esm/core/phone-auth/strategies/link.js +97 -13
- package/dist/esm/core/phone-auth/strategies/ts43.d.ts +19 -0
- package/dist/esm/core/phone-auth/strategies/ts43.js +33 -2
- package/dist/esm/core/phone-auth/strategies/types.d.ts +13 -0
- package/dist/esm/core/phone-auth/strategies/types.js +4 -0
- package/dist/esm/core/phone-auth/type-guards.d.ts +128 -0
- package/dist/esm/core/phone-auth/type-guards.js +131 -0
- package/dist/esm/core/phone-auth/types.d.ts +113 -0
- package/dist/esm/core/phone-auth/types.js +32 -0
- package/dist/esm/core/phone-auth/ui/mobile-debug-console.d.ts +4 -0
- package/dist/esm/core/phone-auth/ui/mobile-debug-console.js +28 -2
- package/dist/esm/core/phone-auth/ui/modal.d.ts +68 -27
- package/dist/esm/core/phone-auth/ui/modal.js +422 -889
- package/dist/esm/core/phone-auth/validation-utils.d.ts +26 -4
- package/dist/esm/core/phone-auth/validation-utils.js +34 -24
- package/dist/esm/core/types.d.ts +35 -0
- package/dist/esm/core/version.js +2 -1
- package/dist/esm/index.js +9 -1
- package/dist/index.js +7 -0
- package/package.json +1 -1
- package/dist/browser/web-client-sdk.min.js.LICENSE.txt +0 -1
|
@@ -7,10 +7,11 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
+
// Import API types for API communication
|
|
10
11
|
import * as API from './api-types';
|
|
11
12
|
import { BrowserError, BrowserErrorCode, BrowserName } from './types';
|
|
12
13
|
import { PhoneAuthErrorCode, isPhoneAuthError, parseBackendError, getUserMessage, isUserError, serializeError, createErrorBreadcrumb } from './error-utils';
|
|
13
|
-
import { validatePhoneNumber, validatePlmn
|
|
14
|
+
import { validatePhoneNumber, validatePlmn } from './validation-utils';
|
|
14
15
|
import { LoggerFactory } from '../logger';
|
|
15
16
|
import { DesktopHandler } from './strategies/desktop';
|
|
16
17
|
import { LinkHandler } from './strategies/link';
|
|
@@ -21,31 +22,41 @@ export class PhoneAuthClient {
|
|
|
21
22
|
this.crossDeviceActive = false;
|
|
22
23
|
this.retryCount = 0;
|
|
23
24
|
this.sessionCache = new Map();
|
|
25
|
+
// Store base timeout for normal operations
|
|
24
26
|
this.baseTimeout = config.timeout || 30000;
|
|
27
|
+
// Default configuration with cross-device support
|
|
25
28
|
this.config = {
|
|
26
29
|
endpoints: {
|
|
27
30
|
prepare: ((_a = config.endpoints) === null || _a === void 0 ? void 0 : _a.prepare) || '/api/magic-auth/prepare',
|
|
28
31
|
process: ((_b = config.endpoints) === null || _b === void 0 ? void 0 : _b.process) || '/api/magic-auth/process',
|
|
29
|
-
polling: (_c = config.endpoints) === null || _c === void 0 ? void 0 : _c.polling
|
|
32
|
+
polling: (_c = config.endpoints) === null || _c === void 0 ? void 0 : _c.polling // Pass through the polling endpoint
|
|
30
33
|
},
|
|
31
34
|
timeout: config.timeout || 30000,
|
|
32
|
-
pollingInterval: config.pollingInterval || 2000,
|
|
33
|
-
maxPollingAttempts: config.maxPollingAttempts || 30,
|
|
35
|
+
pollingInterval: config.pollingInterval || 2000, // Default 2 seconds
|
|
36
|
+
maxPollingAttempts: config.maxPollingAttempts || 30, // Changed from 150 to 30 (1 minute total)
|
|
34
37
|
debug: config.debug || false,
|
|
35
38
|
aggregatorId: config.aggregatorId || 'default',
|
|
39
|
+
devEnv: config.devEnv,
|
|
36
40
|
devtools: config.devtools
|
|
37
41
|
};
|
|
38
42
|
this.debug = this.config.debug;
|
|
43
|
+
// Log initialization with devEnv if set
|
|
44
|
+
if (config.devEnv) {
|
|
45
|
+
console.log(`[PhoneAuth] Initialized with devEnv: ${config.devEnv}`);
|
|
46
|
+
}
|
|
47
|
+
// Store callbacks
|
|
39
48
|
this.callbacks = {
|
|
40
49
|
onCrossDeviceDetected: config.onCrossDeviceDetected,
|
|
41
50
|
onRetryAttempt: config.onRetryAttempt
|
|
42
51
|
};
|
|
52
|
+
// Initialize logger based on config
|
|
43
53
|
this.logger = LoggerFactory.create({
|
|
44
54
|
level: config.logLevel,
|
|
45
55
|
prefix: '[PhoneAuth]',
|
|
46
56
|
remote: config.remoteLogging,
|
|
47
57
|
custom: config.logger
|
|
48
58
|
});
|
|
59
|
+
// Initialize developer tools if configured
|
|
49
60
|
if (((_d = config.devtools) === null || _d === void 0 ? void 0 : _d.showMobileConsole) && typeof window !== 'undefined') {
|
|
50
61
|
import('./ui/mobile-debug-console').then(({ MobileDebugConsole }) => {
|
|
51
62
|
MobileDebugConsole.init();
|
|
@@ -54,21 +65,32 @@ export class PhoneAuthClient {
|
|
|
54
65
|
console.error('[PhoneAuth] Failed to load mobile debug console:', err);
|
|
55
66
|
});
|
|
56
67
|
}
|
|
68
|
+
// Set up session cache cleanup
|
|
57
69
|
this.setupCacheCleanup();
|
|
58
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Get user-friendly error message using error utilities
|
|
73
|
+
*/
|
|
59
74
|
getUserFriendlyMessage(error) {
|
|
60
75
|
if (typeof error === 'string') {
|
|
76
|
+
// For legacy string error codes
|
|
61
77
|
return getUserMessage({ code: error });
|
|
62
78
|
}
|
|
63
79
|
return getUserMessage(error);
|
|
64
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Log error with proper context and sanitization
|
|
83
|
+
*/
|
|
65
84
|
logError(error, context) {
|
|
85
|
+
// Create breadcrumb for error tracking
|
|
66
86
|
const breadcrumb = createErrorBreadcrumb(error);
|
|
87
|
+
// Serialize error for logging (sanitized)
|
|
67
88
|
const serialized = serializeError(error);
|
|
68
89
|
if (this.debug || !isUserError(error)) {
|
|
69
90
|
console.error('[PhoneAuth] Error:', Object.assign(Object.assign({}, serialized), { breadcrumb,
|
|
70
91
|
context }));
|
|
71
92
|
}
|
|
93
|
+
// Log trace context for distributed tracing (if available)
|
|
72
94
|
if (error.traceId) {
|
|
73
95
|
console.debug('[PhoneAuth] Trace Context:', {
|
|
74
96
|
traceId: error.traceId,
|
|
@@ -77,11 +99,20 @@ export class PhoneAuthClient {
|
|
|
77
99
|
});
|
|
78
100
|
}
|
|
79
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Check if the browser supports secure phone authentication
|
|
104
|
+
*/
|
|
80
105
|
isSupported() {
|
|
106
|
+
// Only check on client side
|
|
81
107
|
if (typeof window === 'undefined')
|
|
82
108
|
return false;
|
|
109
|
+
// Check for the DigitalCredential constructor specifically
|
|
110
|
+
// This is more accurate than checking credentials.get which exists for other credential types
|
|
83
111
|
return 'DigitalCredential' in window;
|
|
84
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Get detailed browser support information
|
|
115
|
+
*/
|
|
85
116
|
getBrowserSupportInfo() {
|
|
86
117
|
if (typeof window === 'undefined') {
|
|
87
118
|
return {
|
|
@@ -100,6 +131,7 @@ export class PhoneAuthClient {
|
|
|
100
131
|
browser: isChrome ? BrowserName.CHROME : isEdge ? BrowserName.EDGE : BrowserName.OTHER
|
|
101
132
|
};
|
|
102
133
|
}
|
|
134
|
+
// Provide specific guidance based on browser
|
|
103
135
|
if (isChrome || isEdge) {
|
|
104
136
|
return {
|
|
105
137
|
supported: false,
|
|
@@ -116,11 +148,16 @@ export class PhoneAuthClient {
|
|
|
116
148
|
message: 'Your browser doesn\'t support the Digital Credentials API. Please use Chrome or Edge with the #web-identity-digital-credentials flag enabled.'
|
|
117
149
|
};
|
|
118
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Main verification method with silent retry support
|
|
153
|
+
*/
|
|
119
154
|
verify(options) {
|
|
120
155
|
return __awaiter(this, void 0, void 0, function* () {
|
|
156
|
+
// Reset retry count for new verification
|
|
121
157
|
this.retryCount = 0;
|
|
122
158
|
this.lastRequest = options;
|
|
123
|
-
const maxRetries = 2;
|
|
159
|
+
const maxRetries = 2; // Default max retries
|
|
160
|
+
// Try verification with silent retries
|
|
124
161
|
return this.verifyWithRetry(options, maxRetries);
|
|
125
162
|
});
|
|
126
163
|
}
|
|
@@ -128,38 +165,55 @@ export class PhoneAuthClient {
|
|
|
128
165
|
return __awaiter(this, void 0, void 0, function* () {
|
|
129
166
|
var _a, _b;
|
|
130
167
|
try {
|
|
168
|
+
// Step 1: Prepare the phone verification request
|
|
131
169
|
const preparedRequest = yield this.preparePhoneRequest(options);
|
|
170
|
+
// Step 2: Invoke secure prompt for user consent (always in UI mode for high-level API)
|
|
132
171
|
const credentialResponse = yield this.invokeSecurePrompt(preparedRequest);
|
|
172
|
+
// Check if headless result was returned (this shouldn't happen in high-level API)
|
|
133
173
|
if (credentialResponse && typeof credentialResponse === 'object' && 'strategy' in credentialResponse) {
|
|
134
174
|
throw this.createError(PhoneAuthErrorCode.INVALID_RESPONSE, 'Headless mode is not supported in authenticatePhoneNumber. Use preparePhoneRequest and invokeSecurePrompt directly for headless mode.');
|
|
135
175
|
}
|
|
176
|
+
// Step 3: Process the response through appropriate endpoint
|
|
136
177
|
const credential = credentialResponse;
|
|
178
|
+
// Validate use_case is provided for endpoint selection
|
|
137
179
|
if (!options.use_case) {
|
|
138
180
|
throw this.createError(PhoneAuthErrorCode.MISSING_PARAMETERS, 'use_case is required', { field: 'use_case' });
|
|
139
181
|
}
|
|
140
182
|
const result = options.use_case === API.USE_CASE.GET_PHONE_NUMBER
|
|
141
183
|
? yield this.getPhoneNumber(credential, preparedRequest.session)
|
|
142
184
|
: yield this.verifyPhoneNumber(credential, preparedRequest.session);
|
|
185
|
+
// Return the result directly - it's already the correct type
|
|
186
|
+
// Cache successful result with session info for later use
|
|
143
187
|
this.cacheSession(options, result);
|
|
144
188
|
return result;
|
|
145
189
|
}
|
|
146
190
|
catch (error) {
|
|
147
191
|
const authError = isPhoneAuthError(error) ? error : parseBackendError(error);
|
|
192
|
+
// Check if we should retry (silent retry - don't throw yet)
|
|
193
|
+
// Note: We cannot automatically retry USER_DENIED errors because the Digital Credentials API
|
|
194
|
+
// requires user interaction (transient activation). Automatic retries would fail with
|
|
195
|
+
// "The 'digital-credentials-get' feature requires transient activation" error.
|
|
148
196
|
if (this.shouldRetry(authError) && this.retryCount < maxRetries) {
|
|
149
197
|
this.retryCount++;
|
|
198
|
+
// Notify about retry attempt (but don't show error to user)
|
|
150
199
|
(_b = (_a = this.callbacks).onRetryAttempt) === null || _b === void 0 ? void 0 : _b.call(_a, this.retryCount, maxRetries);
|
|
151
200
|
if (this.debug) {
|
|
152
201
|
console.log(`[PhoneAuth] Retrying verification (attempt ${this.retryCount + 1}/${maxRetries + 1})`);
|
|
153
202
|
}
|
|
154
|
-
|
|
203
|
+
// Wait before retry
|
|
204
|
+
yield this.delay(Math.min(1000 * Math.pow(2, this.retryCount - 1), 5000)); // Exponential backoff
|
|
205
|
+
// Check cache for recent successful session
|
|
155
206
|
const cachedResult = this.getCachedSession(options);
|
|
156
207
|
if (cachedResult) {
|
|
157
208
|
if (this.debug)
|
|
158
209
|
console.log('[PhoneAuth] Using cached session result');
|
|
159
210
|
return cachedResult;
|
|
160
211
|
}
|
|
212
|
+
// Retry the verification
|
|
161
213
|
return this.verifyWithRetry(options, maxRetries);
|
|
162
214
|
}
|
|
215
|
+
// All retries exhausted or non-retryable error - now throw
|
|
216
|
+
// Add context
|
|
163
217
|
authError.context = {
|
|
164
218
|
step: 'complete',
|
|
165
219
|
useCase: options.use_case,
|
|
@@ -168,11 +222,15 @@ export class PhoneAuthClient {
|
|
|
168
222
|
attemptNumber: this.retryCount + 1,
|
|
169
223
|
maxAttempts: maxRetries + 1
|
|
170
224
|
};
|
|
225
|
+
// Log error with proper sanitization
|
|
171
226
|
this.logError(authError, { options });
|
|
227
|
+
// Re-throw the structured error
|
|
172
228
|
if (isPhoneAuthError(error)) {
|
|
229
|
+
// If it already has context, throw as-is
|
|
173
230
|
if (error.context) {
|
|
174
231
|
throw error;
|
|
175
232
|
}
|
|
233
|
+
// Otherwise, create a new error with context
|
|
176
234
|
const enhancedError = {
|
|
177
235
|
code: authError.code,
|
|
178
236
|
message: error.message,
|
|
@@ -197,35 +255,60 @@ export class PhoneAuthClient {
|
|
|
197
255
|
}
|
|
198
256
|
});
|
|
199
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* High-level method to get phone number (complete flow)
|
|
260
|
+
* Handles prepare, credential prompt, and get phone number in one call
|
|
261
|
+
*/
|
|
200
262
|
getPhoneNumberComplete(options) {
|
|
201
263
|
return __awaiter(this, void 0, void 0, function* () {
|
|
202
264
|
return this.verify(Object.assign({ use_case: API.USE_CASE.GET_PHONE_NUMBER }, options));
|
|
203
265
|
});
|
|
204
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* High-level method to verify phone number (complete flow)
|
|
269
|
+
* Handles prepare, credential prompt, and verification in one call
|
|
270
|
+
*/
|
|
205
271
|
verifyPhoneNumberComplete(phoneNumber, options) {
|
|
206
272
|
return __awaiter(this, void 0, void 0, function* () {
|
|
207
273
|
return this.verify(Object.assign({ use_case: API.USE_CASE.VERIFY_PHONE_NUMBER, phone_number: phoneNumber }, options));
|
|
208
274
|
});
|
|
209
275
|
}
|
|
276
|
+
/**
|
|
277
|
+
* Step 1: Prepare phone verification request
|
|
278
|
+
*
|
|
279
|
+
* This method prepares a secure request for phone verification.
|
|
280
|
+
* You can use this with your own backend or the glide-sdk-node.
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* ```typescript
|
|
284
|
+
* const request = await phoneAuthClient.preparePhoneRequest({ useCase: 'GetPhoneNumber' });
|
|
285
|
+
* // Handle the request with custom logic
|
|
286
|
+
* ```
|
|
287
|
+
*/
|
|
210
288
|
preparePhoneRequest(options) {
|
|
211
289
|
return __awaiter(this, void 0, void 0, function* () {
|
|
212
290
|
var _a, _b, _c;
|
|
291
|
+
// Validate phone number if provided
|
|
213
292
|
if (options.phone_number) {
|
|
214
293
|
const phoneValidation = validatePhoneNumber(options.phone_number);
|
|
215
294
|
if (!phoneValidation.valid) {
|
|
216
295
|
throw this.createError(PhoneAuthErrorCode.INVALID_PHONE_NUMBER, phoneValidation.error, { field: 'phone_number' });
|
|
217
296
|
}
|
|
218
297
|
}
|
|
298
|
+
// Validate PLMN if provided
|
|
219
299
|
if (options.plmn) {
|
|
220
300
|
const plmnValidation = validatePlmn(options.plmn);
|
|
221
301
|
if (!plmnValidation.valid) {
|
|
222
302
|
throw this.createError(PhoneAuthErrorCode.BAD_REQUEST, plmnValidation.error, { field: 'plmn' });
|
|
223
303
|
}
|
|
224
304
|
}
|
|
305
|
+
// Validate use_case is provided (unless only parent_session_id is given)
|
|
225
306
|
if (!options.use_case && !(((_a = options.options) === null || _a === void 0 ? void 0 : _a.parent_session_id) && !options.phone_number && !options.plmn)) {
|
|
226
307
|
throw this.createError(PhoneAuthErrorCode.MISSING_PARAMETERS, 'use_case is required', { field: 'use_case' });
|
|
227
308
|
}
|
|
309
|
+
// Validate required parameters based on use case
|
|
228
310
|
if (!options.phone_number && !options.plmn && !((_b = options.options) === null || _b === void 0 ? void 0 : _b.parent_session_id)) {
|
|
311
|
+
// Provide specific error message based on use case
|
|
229
312
|
if (options.use_case === API.USE_CASE.GET_PHONE_NUMBER) {
|
|
230
313
|
throw this.createError(PhoneAuthErrorCode.MISSING_PARAMETERS, 'PLMN (MCC/MNC) is required for GetPhoneNumber. Please provide carrier network information.', { field: 'plmn', useCase: 'GetPhoneNumber' });
|
|
231
314
|
}
|
|
@@ -233,36 +316,39 @@ export class PhoneAuthClient {
|
|
|
233
316
|
throw this.createError(PhoneAuthErrorCode.MISSING_PARAMETERS, 'Phone number is required for VerifyPhoneNumber', { field: 'phoneNumber', useCase: 'VerifyPhoneNumber' });
|
|
234
317
|
}
|
|
235
318
|
else {
|
|
319
|
+
// Fallback for other use cases
|
|
236
320
|
throw this.createError(PhoneAuthErrorCode.MISSING_PARAMETERS, 'Either phone number or PLMN (MCC/MNC) must be provided', { field: 'phoneNumber,plmn' });
|
|
237
321
|
}
|
|
238
322
|
}
|
|
323
|
+
// Log parent session usage
|
|
239
324
|
if (((_c = options.options) === null || _c === void 0 ? void 0 : _c.parent_session_id) && !options.phone_number && !options.plmn) {
|
|
240
325
|
if (this.debug) {
|
|
241
326
|
console.log('[PhoneAuth] Using parent_session_id: %s, use_case: %s', options.options.parent_session_id, options.use_case || 'not provided');
|
|
242
327
|
}
|
|
243
328
|
}
|
|
244
329
|
const requestId = `web-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
.replace(/\//g, '_')
|
|
248
|
-
.replace(/=/g, '');
|
|
249
|
-
const nonceValidation = validateNonce(nonce);
|
|
250
|
-
if (!nonceValidation.valid) {
|
|
251
|
-
throw this.createError(PhoneAuthErrorCode.INTERNAL_SERVER_ERROR, 'Failed to generate valid nonce', { field: 'nonce' });
|
|
252
|
-
}
|
|
330
|
+
// Build properly typed request body according to API specification
|
|
331
|
+
// Be permissive - backend will ignore extra fields if not needed
|
|
253
332
|
const requestBody = {
|
|
333
|
+
// Include use_case if provided (optional when parent_session_id is given)
|
|
254
334
|
use_case: options.use_case,
|
|
335
|
+
// Include phone_number if provided (backend ignores if not needed)
|
|
255
336
|
phone_number: options.phone_number,
|
|
337
|
+
// Include PLMN if provided
|
|
256
338
|
plmn: options.plmn ? {
|
|
257
339
|
mcc: options.plmn.mcc,
|
|
258
340
|
mnc: options.plmn.mnc
|
|
259
341
|
} : undefined,
|
|
260
|
-
|
|
342
|
+
// Auto-generated request ID
|
|
261
343
|
id: requestId,
|
|
344
|
+
// Optional fields
|
|
262
345
|
client_info: {
|
|
263
346
|
user_agent: navigator.userAgent,
|
|
264
|
-
platform: navigator.platform
|
|
347
|
+
platform: navigator.platform,
|
|
348
|
+
// user_agent: "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36",
|
|
349
|
+
// platform: "Android"
|
|
265
350
|
},
|
|
351
|
+
// Advanced options (for desktop-mobile binding and future features)
|
|
266
352
|
options: options.options
|
|
267
353
|
};
|
|
268
354
|
this.log('Preparing phone verification request', requestBody);
|
|
@@ -273,15 +359,20 @@ export class PhoneAuthClient {
|
|
|
273
359
|
body: JSON.stringify(requestBody)
|
|
274
360
|
});
|
|
275
361
|
if (!response.ok) {
|
|
362
|
+
// Try to get error details from response body
|
|
276
363
|
let errorDetails = null;
|
|
277
364
|
try {
|
|
278
365
|
errorDetails = yield response.json();
|
|
366
|
+
// Always include the HTTP status from the response
|
|
279
367
|
errorDetails = Object.assign(Object.assign({}, errorDetails), { status: response.status });
|
|
280
368
|
}
|
|
281
369
|
catch (_d) {
|
|
370
|
+
// If JSON parsing fails, use status text
|
|
282
371
|
errorDetails = { status: response.status, statusText: response.statusText };
|
|
283
372
|
}
|
|
373
|
+
// Parse the backend error response (handles both structured and unstructured errors)
|
|
284
374
|
const parsedError = parseBackendError(errorDetails);
|
|
375
|
+
// Enhance with additional context
|
|
285
376
|
parsedError.context = {
|
|
286
377
|
step: 'prepare',
|
|
287
378
|
useCase: options.use_case,
|
|
@@ -289,6 +380,7 @@ export class PhoneAuthClient {
|
|
|
289
380
|
userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : undefined,
|
|
290
381
|
url: typeof window !== 'undefined' ? window.location.href : undefined
|
|
291
382
|
};
|
|
383
|
+
// Add endpoint info
|
|
292
384
|
parsedError.details = Object.assign(Object.assign({}, parsedError.details), { endpoint: 'prepare', status: response.status });
|
|
293
385
|
throw parsedError;
|
|
294
386
|
}
|
|
@@ -297,12 +389,16 @@ export class PhoneAuthClient {
|
|
|
297
389
|
if (!data.authentication_strategy || !data.data || !data.session) {
|
|
298
390
|
throw this.createError(PhoneAuthErrorCode.INVALID_RESPONSE, 'Invalid response format from backend');
|
|
299
391
|
}
|
|
392
|
+
// Return the full response as-is
|
|
393
|
+
// The invoke method will handle it based on authentication_strategy
|
|
300
394
|
return data;
|
|
301
395
|
}
|
|
302
396
|
catch (error) {
|
|
397
|
+
// If it's already an AuthError, re-throw it
|
|
303
398
|
if (this.isAuthError(error)) {
|
|
304
399
|
throw error;
|
|
305
400
|
}
|
|
401
|
+
// Otherwise, wrap it as a network error
|
|
306
402
|
throw this.createError(PhoneAuthErrorCode.NETWORK_ERROR, 'Failed to prepare verification request', {
|
|
307
403
|
originalError: error,
|
|
308
404
|
context: {
|
|
@@ -316,16 +412,68 @@ export class PhoneAuthClient {
|
|
|
316
412
|
}
|
|
317
413
|
});
|
|
318
414
|
}
|
|
415
|
+
/**
|
|
416
|
+
* Step 2: Invoke secure prompt for user consent
|
|
417
|
+
*
|
|
418
|
+
* This method can work in two modes:
|
|
419
|
+
* 1. **UI Mode (default)**: Shows built-in UI components (modals/buttons)
|
|
420
|
+
* 2. **Headless Mode**: Returns raw data for custom UI implementation
|
|
421
|
+
*
|
|
422
|
+
* **Important**: This method automatically handles reactive objects from frameworks
|
|
423
|
+
* like Vue.js and React by deep cloning the input. This ensures compatibility with
|
|
424
|
+
* browser APIs that expect plain objects.
|
|
425
|
+
*
|
|
426
|
+
* @example UI Mode (shows modal/button)
|
|
427
|
+
* ```typescript
|
|
428
|
+
* // Shows SDK's built-in UI
|
|
429
|
+
* const credential = await phoneAuth.invokeSecurePrompt(prepareResult);
|
|
430
|
+
*
|
|
431
|
+
* // Customize the UI
|
|
432
|
+
* const credential = await phoneAuth.invokeSecurePrompt(prepareResult, {
|
|
433
|
+
* modalOptions: {
|
|
434
|
+
* title: 'Verify Your Identity',
|
|
435
|
+
* buttonText: 'Continue with Verizon'
|
|
436
|
+
* }
|
|
437
|
+
* });
|
|
438
|
+
* ```
|
|
439
|
+
*
|
|
440
|
+
* @example Extended Mode (returns control methods)
|
|
441
|
+
* ```typescript
|
|
442
|
+
* // Get control methods for custom implementation
|
|
443
|
+
* const result = await phoneAuth.invokeSecurePrompt(prepareResult, {
|
|
444
|
+
* executionMode: 'extended',
|
|
445
|
+
* preventDefaultUI: true // Desktop: no modal
|
|
446
|
+
* });
|
|
447
|
+
*
|
|
448
|
+
* if (result.strategy === 'desktop') {
|
|
449
|
+
* // Show custom QR UI
|
|
450
|
+
* showCustomQR(result.qr_code_data);
|
|
451
|
+
* // Start polling
|
|
452
|
+
* await result.start_polling();
|
|
453
|
+
* }
|
|
454
|
+
* ```
|
|
455
|
+
*
|
|
456
|
+
* @param prepareResponse - Response from prepare() with strategy and data
|
|
457
|
+
* @param options - Control UI behavior and response type
|
|
458
|
+
* @returns Credential or ExtendedResponse based on executionMode
|
|
459
|
+
*/
|
|
319
460
|
invokeSecurePrompt(prepareResponse, options) {
|
|
320
461
|
return __awaiter(this, void 0, void 0, function* () {
|
|
462
|
+
// Deep clone to avoid issues with reactive objects (Vue/React)
|
|
463
|
+
// This ensures we work with plain objects for browser APIs
|
|
464
|
+
// Vue's reactivity system wraps objects in Proxies which can interfere
|
|
465
|
+
// with browser APIs like Digital Credentials API that expect plain objects
|
|
321
466
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
|
|
467
|
+
// Try structuredClone first (modern browsers), but catch errors and fallback to JSON method
|
|
322
468
|
let plainResponse;
|
|
323
469
|
try {
|
|
470
|
+
// structuredClone might throw if object contains non-cloneable properties
|
|
324
471
|
plainResponse = typeof structuredClone !== 'undefined'
|
|
325
472
|
? structuredClone(prepareResponse)
|
|
326
473
|
: JSON.parse(JSON.stringify(prepareResponse));
|
|
327
474
|
}
|
|
328
475
|
catch (cloneError) {
|
|
476
|
+
// Fallback to JSON method if structuredClone fails
|
|
329
477
|
if (this.debug) {
|
|
330
478
|
console.log('[PhoneAuth] structuredClone failed, using JSON fallback:', cloneError);
|
|
331
479
|
}
|
|
@@ -335,11 +483,16 @@ export class PhoneAuthClient {
|
|
|
335
483
|
console.log('[PhoneAuth] Session cache size:', this.sessionCache.size);
|
|
336
484
|
console.log('[PhoneAuth] Retry count:', this.retryCount);
|
|
337
485
|
console.log('[PhoneAuth] PrepareResponse received:', JSON.stringify(plainResponse, null, 2));
|
|
486
|
+
// Treat options as InvokeOptions (the modern format)
|
|
487
|
+
// Legacy DesktopAuthOptions properties will still work through property access
|
|
338
488
|
const opts = options;
|
|
489
|
+
// Get configuration from options - access properties directly
|
|
339
490
|
const strategy = plainResponse.authentication_strategy;
|
|
340
491
|
const preventDefaultUI = (_a = opts === null || opts === void 0 ? void 0 : opts.preventDefaultUI) !== null && _a !== void 0 ? _a : false;
|
|
341
492
|
const executionMode = (_b = opts === null || opts === void 0 ? void 0 : opts.executionMode) !== null && _b !== void 0 ? _b : 'standard';
|
|
493
|
+
// Handle based on authentication strategy
|
|
342
494
|
if (plainResponse.authentication_strategy === API.AUTHENTICATION_STRATEGY.TS43) {
|
|
495
|
+
// Check browser support for TS43 strategy which requires Digital Credentials API
|
|
343
496
|
if (!this.isSupported()) {
|
|
344
497
|
throw this.createError(PhoneAuthErrorCode.BROWSER_NOT_SUPPORTED, 'Your browser does not support the Digital Credentials API required for TS43 authentication');
|
|
345
498
|
}
|
|
@@ -353,13 +506,18 @@ export class PhoneAuthClient {
|
|
|
353
506
|
}
|
|
354
507
|
};
|
|
355
508
|
this.log('Invoking TS43 secure authentication prompt', secureCredentialRequest);
|
|
509
|
+
// Function to trigger TS43 authentication
|
|
356
510
|
const triggerTS43 = () => __awaiter(this, void 0, void 0, function* () {
|
|
357
511
|
var _a, _b;
|
|
358
512
|
try {
|
|
513
|
+
// This is the browser API call for TS43
|
|
514
|
+
// Cast to CredentialRequestOptions with digital field (TS43 specific)
|
|
359
515
|
const credentialOptions = secureCredentialRequest;
|
|
360
516
|
const credentialResponse = yield navigator.credentials.get(credentialOptions);
|
|
517
|
+
// Type guard for Digital Credential response
|
|
361
518
|
const digitalResponse = credentialResponse;
|
|
362
519
|
if (!digitalResponse || !('data' in digitalResponse) || !digitalResponse.data) {
|
|
520
|
+
// Check if this is likely due to the browser flag being disabled
|
|
363
521
|
const supportInfo = this.getBrowserSupportInfo();
|
|
364
522
|
if (supportInfo.browser === BrowserName.CHROME || supportInfo.browser === BrowserName.EDGE) {
|
|
365
523
|
throw new Error(`Digital Credentials API returned no response. This usually means the browser feature flag is not enabled. Please ensure the ${supportInfo.helpUrl || '#web-identity-digital-credentials flag'} is set to "Enabled" (not "Default" or "Disabled") and restart your browser.`);
|
|
@@ -371,6 +529,7 @@ export class PhoneAuthClient {
|
|
|
371
529
|
return credentialData.vp_token;
|
|
372
530
|
}
|
|
373
531
|
catch (error) {
|
|
532
|
+
// Capture detailed browser error information
|
|
374
533
|
const errorObj = error;
|
|
375
534
|
const browserErrorDetails = {
|
|
376
535
|
name: errorObj.name || 'UnknownError',
|
|
@@ -383,9 +542,11 @@ export class PhoneAuthClient {
|
|
|
383
542
|
timestamp: new Date().toISOString(),
|
|
384
543
|
userAgent: navigator.userAgent,
|
|
385
544
|
url: window.location.href,
|
|
545
|
+
// Include request details for debugging
|
|
386
546
|
authentication_strategy: plainResponse.authentication_strategy,
|
|
387
547
|
hasSession: !!plainResponse.session
|
|
388
548
|
};
|
|
549
|
+
// Handle specific browser errors
|
|
389
550
|
if (errorObj.name === BrowserError.NOT_ALLOWED) {
|
|
390
551
|
throw this.createError(PhoneAuthErrorCode.USER_DENIED, 'User denied the credential request or the request timed out', {
|
|
391
552
|
originalError: error,
|
|
@@ -393,6 +554,7 @@ export class PhoneAuthClient {
|
|
|
393
554
|
context: errorContext
|
|
394
555
|
});
|
|
395
556
|
}
|
|
557
|
+
// NetworkError with code 19 specifically indicates user cancellation in Digital Credentials API
|
|
396
558
|
if (errorObj.name === BrowserError.NETWORK && errorObj.code === BrowserErrorCode.USER_CANCELLED_DC_API) {
|
|
397
559
|
throw this.createError(PhoneAuthErrorCode.USER_DENIED, 'Authentication cancelled by user', {
|
|
398
560
|
originalError: error,
|
|
@@ -400,6 +562,7 @@ export class PhoneAuthClient {
|
|
|
400
562
|
context: errorContext
|
|
401
563
|
});
|
|
402
564
|
}
|
|
565
|
+
// NetworkError without code 19 is a real network error
|
|
403
566
|
if (errorObj.name === BrowserError.NETWORK) {
|
|
404
567
|
throw this.createError(PhoneAuthErrorCode.NETWORK_ERROR, 'Network error occurred while retrieving credentials', {
|
|
405
568
|
originalError: error,
|
|
@@ -421,6 +584,7 @@ export class PhoneAuthClient {
|
|
|
421
584
|
context: errorContext
|
|
422
585
|
});
|
|
423
586
|
}
|
|
587
|
+
// Check for other cancellation patterns
|
|
424
588
|
if (errorObj.name === BrowserError.ABORT ||
|
|
425
589
|
((_a = browserErrorDetails.message) === null || _a === void 0 ? void 0 : _a.includes('The operation was aborted')) ||
|
|
426
590
|
((_b = browserErrorDetails.message) === null || _b === void 0 ? void 0 : _b.includes('User cancelled'))) {
|
|
@@ -430,6 +594,7 @@ export class PhoneAuthClient {
|
|
|
430
594
|
context: errorContext
|
|
431
595
|
});
|
|
432
596
|
}
|
|
597
|
+
// For any other errors, capture all details
|
|
433
598
|
throw this.createError(PhoneAuthErrorCode.INTERNAL_SERVER_ERROR, `Digital Credentials API error: ${errorObj.message || 'Unknown error'}`, {
|
|
434
599
|
originalError: error,
|
|
435
600
|
browserError: browserErrorDetails,
|
|
@@ -437,6 +602,11 @@ export class PhoneAuthClient {
|
|
|
437
602
|
});
|
|
438
603
|
}
|
|
439
604
|
});
|
|
605
|
+
// IMPORTANT: For TS43, we ALWAYS call the API directly without any modal
|
|
606
|
+
// The Digital Credentials API provides its own OS-level UI (drawer/bottom sheet)
|
|
607
|
+
// Adding our own modal would be redundant and confusing for users
|
|
608
|
+
// Unlike Link (which needs a button for iOS App Clips), TS43 just needs direct invocation
|
|
609
|
+
// Enhanced trigger function with callback support
|
|
440
610
|
const enhancedTriggerTS43 = () => __awaiter(this, void 0, void 0, function* () {
|
|
441
611
|
var _a, _b;
|
|
442
612
|
try {
|
|
@@ -456,11 +626,16 @@ export class PhoneAuthClient {
|
|
|
456
626
|
throw error;
|
|
457
627
|
}
|
|
458
628
|
});
|
|
629
|
+
// TS43 always auto-triggers (no SDK UI ever)
|
|
630
|
+
// The Digital Credentials API provides its own OS-level UI
|
|
631
|
+
// Use a wrapper object to allow updating the promise reference
|
|
459
632
|
const credentialWrapper = {
|
|
460
633
|
promise: null
|
|
461
634
|
};
|
|
462
635
|
try {
|
|
636
|
+
// Always try to trigger immediately
|
|
463
637
|
const vpToken = yield enhancedTriggerTS43();
|
|
638
|
+
// Convert to AuthCredential format
|
|
464
639
|
credentialWrapper.promise = Promise.resolve({
|
|
465
640
|
credential: typeof vpToken === 'string' ? vpToken : Object.values(vpToken)[0],
|
|
466
641
|
session: plainResponse.session,
|
|
@@ -468,25 +643,34 @@ export class PhoneAuthClient {
|
|
|
468
643
|
});
|
|
469
644
|
}
|
|
470
645
|
catch (error) {
|
|
646
|
+
// If auto-trigger fails, create a rejected promise
|
|
471
647
|
credentialWrapper.promise = Promise.reject(error);
|
|
472
648
|
}
|
|
649
|
+
// Handle based on execution mode
|
|
473
650
|
if (executionMode === 'extended') {
|
|
651
|
+
// Extended mode - return control methods
|
|
474
652
|
const response = {
|
|
475
653
|
strategy: 'ts43',
|
|
476
654
|
session: plainResponse.session,
|
|
477
|
-
credential: credentialWrapper.promise,
|
|
655
|
+
credential: credentialWrapper.promise, // Initial value
|
|
656
|
+
// Re-trigger credential request
|
|
478
657
|
trigger: () => __awaiter(this, void 0, void 0, function* () {
|
|
479
658
|
const vpToken = yield enhancedTriggerTS43();
|
|
659
|
+
// Update the credential promise in the wrapper
|
|
480
660
|
credentialWrapper.promise = Promise.resolve({
|
|
481
661
|
credential: typeof vpToken === 'string' ? vpToken : Object.values(vpToken)[0],
|
|
482
662
|
session: plainResponse.session,
|
|
483
663
|
authenticated: true
|
|
484
664
|
});
|
|
665
|
+
// Return void as per interface
|
|
485
666
|
}),
|
|
486
667
|
cancel: () => {
|
|
668
|
+
// TS43 doesn't have a way to cancel once triggered
|
|
669
|
+
// but we can reject the promise
|
|
487
670
|
credentialWrapper.promise = Promise.reject(this.createError(PhoneAuthErrorCode.USER_DENIED, 'Authentication cancelled'));
|
|
488
671
|
}
|
|
489
672
|
};
|
|
673
|
+
// Define credential as a getter that always returns the current promise
|
|
490
674
|
Object.defineProperty(response, 'credential', {
|
|
491
675
|
get() {
|
|
492
676
|
return credentialWrapper.promise;
|
|
@@ -497,15 +681,20 @@ export class PhoneAuthClient {
|
|
|
497
681
|
return response;
|
|
498
682
|
}
|
|
499
683
|
else {
|
|
684
|
+
// Standard mode - just return credential
|
|
685
|
+
// Wait for and return the credential
|
|
500
686
|
const credential = yield credentialWrapper.promise;
|
|
687
|
+
// Return in standard format
|
|
501
688
|
return {
|
|
502
689
|
[plainResponse.session.session_key]: credential.credential
|
|
503
690
|
};
|
|
504
691
|
}
|
|
505
692
|
}
|
|
506
693
|
else if (plainResponse.authentication_strategy === API.AUTHENTICATION_STRATEGY.DESKTOP) {
|
|
694
|
+
// Desktop strategy - QR code based authentication
|
|
507
695
|
const desktopData = plainResponse.data;
|
|
508
696
|
const handler = new DesktopHandler();
|
|
697
|
+
// Extract QR code data - convert to QRCodeData format for modal
|
|
509
698
|
const qrCodeData = {
|
|
510
699
|
iosQRCode: ((_c = desktopData.data) === null || _c === void 0 ? void 0 : _c.ios_qr_image) || desktopData.ios_qr_image ||
|
|
511
700
|
((_d = desktopData.data) === null || _d === void 0 ? void 0 : _d.qr_code_image) || desktopData.qr_code_image || desktopData.qr_code || '',
|
|
@@ -513,15 +702,18 @@ export class PhoneAuthClient {
|
|
|
513
702
|
iosUrl: ((_f = desktopData.data) === null || _f === void 0 ? void 0 : _f.ios_url) || desktopData.ios_url,
|
|
514
703
|
androidUrl: ((_g = desktopData.data) === null || _g === void 0 ? void 0 : _g.android_url) || desktopData.android_url
|
|
515
704
|
};
|
|
705
|
+
// Also keep snake_case format for extended response
|
|
516
706
|
const qrCodeDataSnakeCase = {
|
|
517
707
|
ios_qr_image: ((_h = desktopData.data) === null || _h === void 0 ? void 0 : _h.ios_qr_image) || desktopData.ios_qr_image,
|
|
518
708
|
android_qr_image: ((_j = desktopData.data) === null || _j === void 0 ? void 0 : _j.android_qr_image) || desktopData.android_qr_image,
|
|
519
709
|
qr_code: ((_k = desktopData.data) === null || _k === void 0 ? void 0 : _k.qr_code_image) || desktopData.qr_code_image || desktopData.qr_code,
|
|
520
710
|
challenge: (_l = desktopData.data) === null || _l === void 0 ? void 0 : _l.challenge
|
|
521
711
|
};
|
|
712
|
+
// Polling options - gather from any options format
|
|
522
713
|
const pollingEndpointToUse = (opts === null || opts === void 0 ? void 0 : opts.pollingEndpoint) ||
|
|
523
714
|
((_m = this.config.endpoints) === null || _m === void 0 ? void 0 : _m.polling);
|
|
524
|
-
const pollingOptions = Object.assign(Object.assign({}, opts), { pollingEndpoint: pollingEndpointToUse, pollingInterval: (opts === null || opts === void 0 ? void 0 : opts.pollingInterval) || this.config.pollingInterval || 2000, maxPollingAttempts: (opts === null || opts === void 0 ? void 0 : opts.maxPollingAttempts) || this.config.maxPollingAttempts || 30, onQRCodeReady: undefined, onStatusUpdate: undefined });
|
|
715
|
+
const pollingOptions = Object.assign(Object.assign({}, opts), { pollingEndpoint: pollingEndpointToUse, pollingInterval: (opts === null || opts === void 0 ? void 0 : opts.pollingInterval) || this.config.pollingInterval || 2000, maxPollingAttempts: (opts === null || opts === void 0 ? void 0 : opts.maxPollingAttempts) || this.config.maxPollingAttempts || 30, devEnv: (opts === null || opts === void 0 ? void 0 : opts.devEnv) || this.config.devEnv, onQRCodeReady: undefined, onStatusUpdate: undefined });
|
|
716
|
+
// Decide whether to show modal based on preventDefaultUI
|
|
525
717
|
const showModal = !preventDefaultUI;
|
|
526
718
|
let modal;
|
|
527
719
|
let modalRef = undefined;
|
|
@@ -533,27 +725,40 @@ export class PhoneAuthClient {
|
|
|
533
725
|
});
|
|
534
726
|
if (showModal) {
|
|
535
727
|
console.log('[Desktop] Creating modal with QR data:', qrCodeData);
|
|
728
|
+
// Create and setup modal
|
|
536
729
|
modal = new AuthModal(opts === null || opts === void 0 ? void 0 : opts.modalOptions, opts === null || opts === void 0 ? void 0 : opts.callbacks);
|
|
537
730
|
modal.setCloseCallback(() => {
|
|
538
731
|
this.log('Desktop QR modal closed by user, cancelling polling');
|
|
539
732
|
handler.cancel();
|
|
540
733
|
});
|
|
734
|
+
// Add UI callbacks to polling options
|
|
541
735
|
pollingOptions.onQRCodeReady = (qrData) => {
|
|
542
736
|
console.log('[Desktop] onQRCodeReady callback triggered:', qrData);
|
|
543
737
|
modal.showQRCode(qrData, 'Scan with your mobile device');
|
|
544
738
|
};
|
|
545
739
|
pollingOptions.onStatusUpdate = (status) => {
|
|
546
|
-
if (status.status === '
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
740
|
+
if (status.status === 'pending') {
|
|
741
|
+
modal.updateStatus('Waiting for authentication...');
|
|
742
|
+
}
|
|
743
|
+
else if (status.status === 'authenticated') {
|
|
744
|
+
modal.updateStatus('Authentication successful!');
|
|
745
|
+
setTimeout(() => modal.close(), 1500);
|
|
746
|
+
}
|
|
747
|
+
else if (status.status === 'expired') {
|
|
748
|
+
modal.updateStatus('QR code expired', true);
|
|
749
|
+
}
|
|
750
|
+
else if (status.status === 'error') {
|
|
751
|
+
modal.updateStatus('Authentication failed', true);
|
|
550
752
|
}
|
|
551
753
|
};
|
|
754
|
+
// Note: We don't show the QR code here. It will be shown by the onQRCodeReady callback
|
|
755
|
+
// that gets triggered immediately when handler.invoke() is called
|
|
552
756
|
modalRef = modal;
|
|
553
757
|
}
|
|
554
758
|
else {
|
|
555
759
|
console.log('[Desktop] Modal not shown - preventDefaultUI is true');
|
|
556
760
|
}
|
|
761
|
+
// Create credential promise
|
|
557
762
|
const startPolling = () => handler.invoke(plainResponse, pollingOptions).then(result => {
|
|
558
763
|
if (result.authenticated && result.credential) {
|
|
559
764
|
return {
|
|
@@ -566,13 +771,22 @@ export class PhoneAuthClient {
|
|
|
566
771
|
throw this.createError(PhoneAuthErrorCode.USER_DENIED, result.error || 'Desktop authentication failed');
|
|
567
772
|
}
|
|
568
773
|
});
|
|
774
|
+
// Handle based on execution mode
|
|
569
775
|
if (executionMode === 'extended') {
|
|
776
|
+
// Extended mode - return control methods
|
|
777
|
+
// Create a promise and always start polling immediately
|
|
570
778
|
const credentialPromise = new Promise((resolve, reject) => {
|
|
779
|
+
// Always start polling immediately in extended mode (consistent with Link)
|
|
780
|
+
// This prevents the "unresolved promise trap" where developers might await
|
|
781
|
+
// the credential before calling start_polling()
|
|
571
782
|
startPolling()
|
|
572
783
|
.then(resolve)
|
|
573
784
|
.catch(reject);
|
|
574
785
|
});
|
|
786
|
+
// Create wrapped functions
|
|
575
787
|
const wrappedStartPolling = () => __awaiter(this, void 0, void 0, function* () {
|
|
788
|
+
// Polling has already started automatically in extended mode
|
|
789
|
+
// This method just returns the existing credential promise for consistency
|
|
576
790
|
return credentialPromise;
|
|
577
791
|
});
|
|
578
792
|
const wrappedStopPolling = () => {
|
|
@@ -585,7 +799,9 @@ export class PhoneAuthClient {
|
|
|
585
799
|
handler.cleanup();
|
|
586
800
|
if (modal)
|
|
587
801
|
modal.close();
|
|
802
|
+
// The credential promise will be rejected by the handler.cancel() call
|
|
588
803
|
};
|
|
804
|
+
// Create the response object with a getter for is_polling
|
|
589
805
|
const response = {
|
|
590
806
|
strategy: 'desktop',
|
|
591
807
|
session: plainResponse.session,
|
|
@@ -595,8 +811,10 @@ export class PhoneAuthClient {
|
|
|
595
811
|
start_polling: wrappedStartPolling,
|
|
596
812
|
stop_polling: wrappedStopPolling,
|
|
597
813
|
cancel: wrappedCancel,
|
|
598
|
-
|
|
814
|
+
// This will be replaced with a getter
|
|
815
|
+
is_polling: false // Initial value, will be overridden by getter
|
|
599
816
|
};
|
|
817
|
+
// Define is_polling as a getter that returns current state
|
|
600
818
|
Object.defineProperty(response, 'is_polling', {
|
|
601
819
|
get() {
|
|
602
820
|
return handler.isPolling();
|
|
@@ -607,8 +825,11 @@ export class PhoneAuthClient {
|
|
|
607
825
|
return response;
|
|
608
826
|
}
|
|
609
827
|
else {
|
|
828
|
+
// Standard mode - return credential when complete
|
|
829
|
+
// Start polling and wait for result
|
|
610
830
|
try {
|
|
611
831
|
const credential = yield startPolling();
|
|
832
|
+
// Extract session ID for compatibility
|
|
612
833
|
let sessionId = 'default';
|
|
613
834
|
if (desktopData && typeof desktopData === 'object') {
|
|
614
835
|
if (desktopData.data && typeof desktopData.data === 'object') {
|
|
@@ -628,8 +849,10 @@ export class PhoneAuthClient {
|
|
|
628
849
|
}
|
|
629
850
|
}
|
|
630
851
|
else if (plainResponse.authentication_strategy === API.AUTHENTICATION_STRATEGY.LINK) {
|
|
852
|
+
// Link strategy - app-based authentication (iOS/Android)
|
|
631
853
|
const linkData = plainResponse.data;
|
|
632
854
|
const handler = new LinkHandler();
|
|
855
|
+
// Create reusable trigger function that ONLY opens the App Clip
|
|
633
856
|
const triggerLink = () => {
|
|
634
857
|
var _a, _b;
|
|
635
858
|
try {
|
|
@@ -649,9 +872,12 @@ export class PhoneAuthClient {
|
|
|
649
872
|
});
|
|
650
873
|
}
|
|
651
874
|
};
|
|
875
|
+
// Link always auto-opens the app (no SDK UI by default)
|
|
876
|
+
// Open immediately to preserve user gesture context
|
|
652
877
|
if ((opts === null || opts === void 0 ? void 0 : opts.autoTrigger) !== false) {
|
|
653
878
|
triggerLink();
|
|
654
879
|
}
|
|
880
|
+
// Start polling in the background
|
|
655
881
|
console.log('[PhoneAuth Client] Link polling config:', {
|
|
656
882
|
fromOptions: opts === null || opts === void 0 ? void 0 : opts.pollingEndpoint,
|
|
657
883
|
fromClientConfig: (_o = this.config.endpoints) === null || _o === void 0 ? void 0 : _o.polling,
|
|
@@ -661,15 +887,20 @@ export class PhoneAuthClient {
|
|
|
661
887
|
pollingEndpoint: (opts === null || opts === void 0 ? void 0 : opts.pollingEndpoint) || ((_q = this.config.endpoints) === null || _q === void 0 ? void 0 : _q.polling),
|
|
662
888
|
pollingInterval: (opts === null || opts === void 0 ? void 0 : opts.pollingInterval) || this.config.pollingInterval || 2000,
|
|
663
889
|
maxPollingAttempts: (opts === null || opts === void 0 ? void 0 : opts.maxPollingAttempts) || this.config.maxPollingAttempts || 30,
|
|
890
|
+
devEnv: (opts === null || opts === void 0 ? void 0 : opts.devEnv) || this.config.devEnv,
|
|
664
891
|
onLinkOpened: undefined,
|
|
665
892
|
onStatusUpdate: undefined
|
|
666
893
|
};
|
|
667
894
|
console.log('[PhoneAuth Client] Final Link polling options:', pollingOptions);
|
|
895
|
+
// Handle based on execution mode
|
|
668
896
|
if (executionMode === 'extended') {
|
|
897
|
+
// Extended mode - return control methods and start polling immediately
|
|
669
898
|
let pollingStarted = false;
|
|
670
899
|
let pollingPromise = null;
|
|
900
|
+
// Start polling immediately (Link always polls automatically unlike Desktop)
|
|
671
901
|
pollingPromise = handler.invoke(plainResponse, pollingOptions);
|
|
672
902
|
pollingStarted = true;
|
|
903
|
+
// Create credential promise from the polling result
|
|
673
904
|
const credentialPromise = pollingPromise.then(result => {
|
|
674
905
|
if (result.authenticated && result.credential) {
|
|
675
906
|
return {
|
|
@@ -682,8 +913,10 @@ export class PhoneAuthClient {
|
|
|
682
913
|
throw this.createError(PhoneAuthErrorCode.USER_DENIED, result.error || 'Link authentication failed');
|
|
683
914
|
}
|
|
684
915
|
});
|
|
916
|
+
// Function to restart polling (for retry scenarios)
|
|
685
917
|
const startPolling = () => __awaiter(this, void 0, void 0, function* () {
|
|
686
918
|
if (!pollingStarted) {
|
|
919
|
+
// This is here for API consistency, but for Link it's already polling
|
|
687
920
|
pollingStarted = true;
|
|
688
921
|
if (!pollingPromise) {
|
|
689
922
|
pollingPromise = handler.invoke(plainResponse, pollingOptions);
|
|
@@ -700,6 +933,7 @@ export class PhoneAuthClient {
|
|
|
700
933
|
throw this.createError(PhoneAuthErrorCode.USER_DENIED, result.error || 'Link authentication failed');
|
|
701
934
|
}
|
|
702
935
|
}
|
|
936
|
+
// If already polling, just return the existing promise result
|
|
703
937
|
const result = yield pollingPromise;
|
|
704
938
|
if (result.authenticated && result.credential) {
|
|
705
939
|
return {
|
|
@@ -719,15 +953,21 @@ export class PhoneAuthClient {
|
|
|
719
953
|
data: {
|
|
720
954
|
app_url: linkData.url
|
|
721
955
|
},
|
|
956
|
+
// Re-open the app link
|
|
722
957
|
trigger: triggerLink,
|
|
958
|
+
// Polling control - now prevents double polling
|
|
723
959
|
start_polling: startPolling,
|
|
724
960
|
stop_polling: () => handler.cleanup(),
|
|
725
961
|
cancel: () => {
|
|
726
962
|
handler.cancel();
|
|
727
963
|
handler.cleanup();
|
|
964
|
+
// Note: For Link, polling is already started, so the promise
|
|
965
|
+
// will be rejected by the handler.cancel() call
|
|
728
966
|
},
|
|
729
|
-
|
|
967
|
+
// This will be replaced with a getter
|
|
968
|
+
is_polling: false // Initial value, will be overridden by getter
|
|
730
969
|
};
|
|
970
|
+
// Define is_polling as a getter that returns current state
|
|
731
971
|
Object.defineProperty(response, 'is_polling', {
|
|
732
972
|
get() {
|
|
733
973
|
return handler.isPolling();
|
|
@@ -738,6 +978,7 @@ export class PhoneAuthClient {
|
|
|
738
978
|
return response;
|
|
739
979
|
}
|
|
740
980
|
else {
|
|
981
|
+
// Standard mode - start polling immediately and wait for completion
|
|
741
982
|
const credentialPromise = handler.invoke(plainResponse, pollingOptions).then(result => {
|
|
742
983
|
if (result.authenticated && result.credential) {
|
|
743
984
|
return {
|
|
@@ -750,28 +991,44 @@ export class PhoneAuthClient {
|
|
|
750
991
|
throw this.createError(PhoneAuthErrorCode.USER_DENIED, result.error || 'Link authentication failed');
|
|
751
992
|
}
|
|
752
993
|
});
|
|
994
|
+
// Wait for credential and return in standard format
|
|
753
995
|
const credential = yield credentialPromise;
|
|
754
996
|
const aggregatorId = this.config.aggregatorId || 'default';
|
|
755
997
|
return { [aggregatorId]: credential.credential };
|
|
756
998
|
}
|
|
757
999
|
}
|
|
758
1000
|
else {
|
|
1001
|
+
// Unknown strategy
|
|
759
1002
|
throw this.createError(PhoneAuthErrorCode.UNSUPPORTED_STRATEGY, `Unknown authentication strategy: ${plainResponse.authentication_strategy}`);
|
|
760
1003
|
}
|
|
761
1004
|
});
|
|
762
1005
|
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Step 3A: Get phone number from credential
|
|
1008
|
+
*
|
|
1009
|
+
* @example
|
|
1010
|
+
* ```typescript
|
|
1011
|
+
* const prepareResp = await phoneAuthClient.preparePhoneRequest({ useCase: 'GetPhoneNumber', plmn: {...} });
|
|
1012
|
+
* const credential = await phoneAuthClient.invokeSecurePrompt(prepareResp);
|
|
1013
|
+
* const result = await phoneAuthClient.getPhoneNumber(credential, prepareResp.session);
|
|
1014
|
+
* console.log(result.phone_number); // +1234567890
|
|
1015
|
+
* ```
|
|
1016
|
+
*/
|
|
763
1017
|
getPhoneNumber(credentialResponse, session) {
|
|
764
1018
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1019
|
+
// Extract credential string
|
|
765
1020
|
const credentialString = this.extractCredentialString(credentialResponse);
|
|
1021
|
+
// Build request body for GetPhoneNumber
|
|
766
1022
|
const requestBody = {
|
|
767
1023
|
session: session,
|
|
768
1024
|
credential: credentialString,
|
|
769
|
-
use_case: API.USE_CASE.GET_PHONE_NUMBER
|
|
1025
|
+
use_case: API.USE_CASE.GET_PHONE_NUMBER // Required for server routing
|
|
770
1026
|
};
|
|
1027
|
+
// Only show full details in debug mode, mask sensitive data otherwise
|
|
771
1028
|
if (this.config.debug) {
|
|
772
1029
|
this.log('Getting phone number from credential', {
|
|
773
1030
|
session: session,
|
|
774
|
-
credential: credentialString ? credentialString.substring(0, 50) + '...' : 'undefined',
|
|
1031
|
+
credential: credentialString ? credentialString.substring(0, 50) + '...' : 'undefined', // Show partial for debugging
|
|
775
1032
|
endpoint: this.config.endpoints.process || '/api/phone-auth/process'
|
|
776
1033
|
});
|
|
777
1034
|
}
|
|
@@ -815,18 +1072,35 @@ export class PhoneAuthClient {
|
|
|
815
1072
|
}
|
|
816
1073
|
});
|
|
817
1074
|
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Step 3B: Verify phone number with credential
|
|
1077
|
+
*
|
|
1078
|
+
* @example
|
|
1079
|
+
* ```typescript
|
|
1080
|
+
* const prepareResp = await phoneAuthClient.preparePhoneRequest({
|
|
1081
|
+
* useCase: 'VerifyPhoneNumber',
|
|
1082
|
+
* phoneNumber: '+1234567890'
|
|
1083
|
+
* });
|
|
1084
|
+
* const credential = await phoneAuthClient.invokeSecurePrompt(prepareResp);
|
|
1085
|
+
* const result = await phoneAuthClient.verifyPhoneNumber(credential, prepareResp.session);
|
|
1086
|
+
* console.log(result.verified); // true
|
|
1087
|
+
* ```
|
|
1088
|
+
*/
|
|
818
1089
|
verifyPhoneNumber(credentialResponse, session) {
|
|
819
1090
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1091
|
+
// Extract credential string
|
|
820
1092
|
const credentialString = this.extractCredentialString(credentialResponse);
|
|
1093
|
+
// Build request body for VerifyPhoneNumber
|
|
821
1094
|
const requestBody = {
|
|
822
1095
|
session: session,
|
|
823
1096
|
credential: credentialString,
|
|
824
|
-
use_case: API.USE_CASE.VERIFY_PHONE_NUMBER
|
|
1097
|
+
use_case: API.USE_CASE.VERIFY_PHONE_NUMBER // Required for server routing
|
|
825
1098
|
};
|
|
1099
|
+
// Only show full details in debug mode, mask sensitive data otherwise
|
|
826
1100
|
if (this.config.debug) {
|
|
827
1101
|
this.log('Verifying phone number with credential', {
|
|
828
1102
|
session: session,
|
|
829
|
-
credential: credentialString ? credentialString.substring(0, 50) + '...' : 'undefined',
|
|
1103
|
+
credential: credentialString ? credentialString.substring(0, 50) + '...' : 'undefined', // Show partial for debugging
|
|
830
1104
|
endpoint: this.config.endpoints.process || '/api/phone-auth/process'
|
|
831
1105
|
});
|
|
832
1106
|
}
|
|
@@ -873,36 +1147,53 @@ export class PhoneAuthClient {
|
|
|
873
1147
|
}
|
|
874
1148
|
});
|
|
875
1149
|
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Helper to extract credential string from various formats
|
|
1152
|
+
*/
|
|
876
1153
|
extractCredentialString(credentialResponse) {
|
|
1154
|
+
// If already a string, return it
|
|
877
1155
|
if (typeof credentialResponse === 'string') {
|
|
878
1156
|
return credentialResponse;
|
|
879
1157
|
}
|
|
1158
|
+
// Extract from vp_token object - try configured aggregatorId first, then 'glide', then 'default'
|
|
880
1159
|
let credential = credentialResponse[this.config.aggregatorId || 'glide'];
|
|
1160
|
+
// Fallback to 'glide' if not found
|
|
881
1161
|
if (!credential && credentialResponse['glide']) {
|
|
882
1162
|
credential = credentialResponse['glide'];
|
|
883
1163
|
}
|
|
1164
|
+
// Fallback to 'default' if still not found
|
|
884
1165
|
if (!credential && credentialResponse['default']) {
|
|
885
1166
|
credential = credentialResponse['default'];
|
|
886
1167
|
}
|
|
1168
|
+
// If still not found, try to get the first available key
|
|
887
1169
|
if (!credential) {
|
|
888
1170
|
const keys = Object.keys(credentialResponse);
|
|
889
1171
|
if (keys.length > 0) {
|
|
890
1172
|
credential = credentialResponse[keys[0]];
|
|
891
1173
|
}
|
|
892
1174
|
}
|
|
1175
|
+
// Convert array to string if needed
|
|
893
1176
|
return Array.isArray(credential) ? credential[0] : credential;
|
|
894
1177
|
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Helper to extract error details from response
|
|
1180
|
+
*/
|
|
895
1181
|
extractErrorDetails(response) {
|
|
896
1182
|
return __awaiter(this, void 0, void 0, function* () {
|
|
897
1183
|
try {
|
|
898
1184
|
const errorData = yield response.json();
|
|
899
|
-
|
|
1185
|
+
// Always include the HTTP status from the response
|
|
1186
|
+
return Object.assign(Object.assign({}, errorData), { status: response.status // Ensure HTTP status is included
|
|
1187
|
+
});
|
|
900
1188
|
}
|
|
901
1189
|
catch (_a) {
|
|
902
1190
|
return { status: response.status, statusText: response.statusText };
|
|
903
1191
|
}
|
|
904
1192
|
});
|
|
905
1193
|
}
|
|
1194
|
+
/**
|
|
1195
|
+
* Fetch with timeout
|
|
1196
|
+
*/
|
|
906
1197
|
fetchWithTimeout(url, options) {
|
|
907
1198
|
return __awaiter(this, void 0, void 0, function* () {
|
|
908
1199
|
const controller = new AbortController();
|
|
@@ -915,18 +1206,25 @@ export class PhoneAuthClient {
|
|
|
915
1206
|
}
|
|
916
1207
|
});
|
|
917
1208
|
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Create an AuthError
|
|
1211
|
+
*/
|
|
918
1212
|
createError(code, message, details) {
|
|
919
1213
|
const error = {
|
|
920
1214
|
code,
|
|
921
1215
|
message,
|
|
922
|
-
details: (details === null || details === void 0 ? void 0 : details.originalError) ? Object.assign(Object.assign({}, details), { originalError: undefined
|
|
1216
|
+
details: (details === null || details === void 0 ? void 0 : details.originalError) ? Object.assign(Object.assign({}, details), { originalError: undefined // Remove the original error object
|
|
1217
|
+
}) : details
|
|
923
1218
|
};
|
|
1219
|
+
// Add browser error details if present
|
|
924
1220
|
if (details === null || details === void 0 ? void 0 : details.browserError) {
|
|
925
1221
|
error.browserError = details.browserError;
|
|
926
1222
|
}
|
|
1223
|
+
// Add context if present
|
|
927
1224
|
if (details === null || details === void 0 ? void 0 : details.context) {
|
|
928
1225
|
error.context = details.context;
|
|
929
1226
|
}
|
|
1227
|
+
// Add other specific fields
|
|
930
1228
|
if (details === null || details === void 0 ? void 0 : details.status) {
|
|
931
1229
|
error.status = details.status;
|
|
932
1230
|
}
|
|
@@ -941,37 +1239,51 @@ export class PhoneAuthClient {
|
|
|
941
1239
|
}
|
|
942
1240
|
return error;
|
|
943
1241
|
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Type guard for AuthError
|
|
1244
|
+
*/
|
|
944
1245
|
isAuthError(error) {
|
|
945
1246
|
return error && typeof error.code === 'string' && typeof error.message === 'string';
|
|
946
1247
|
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Debug logging
|
|
1250
|
+
*/
|
|
947
1251
|
log(...args) {
|
|
948
1252
|
if (this.debug) {
|
|
949
1253
|
console.log('[PhoneAuth]', ...args);
|
|
950
1254
|
}
|
|
951
1255
|
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Determine if an error should trigger a retry
|
|
1258
|
+
*/
|
|
952
1259
|
shouldRetry(error) {
|
|
953
1260
|
var _a, _b, _c;
|
|
1261
|
+
// Don't retry on 4xx client errors (these are not transient)
|
|
954
1262
|
if (error.status && error.status >= 400 && error.status < 500) {
|
|
955
1263
|
return false;
|
|
956
1264
|
}
|
|
1265
|
+
// Don't retry on explicit user denial or unsupported browser
|
|
1266
|
+
// USER_DENIED cannot be retried automatically because the Digital Credentials API
|
|
1267
|
+
// requires user interaction (transient activation)
|
|
957
1268
|
const nonRetryableCodes = [
|
|
958
1269
|
'USER_DENIED',
|
|
959
1270
|
'BROWSER_NOT_SUPPORTED',
|
|
960
1271
|
'INVALID_PHONE_NUMBER',
|
|
961
1272
|
'INVALID_PARAMETERS',
|
|
962
1273
|
'MISSING_PARAMETERS',
|
|
963
|
-
'UNPROCESSABLE_ENTITY',
|
|
964
|
-
'USE_CASE_MISMATCH',
|
|
965
|
-
'PHONE_NUMBER_MISMATCH',
|
|
966
|
-
'VERIFICATION_FAILED',
|
|
967
|
-
'INVALID_CREDENTIAL',
|
|
968
|
-
'CARRIER_NOT_ELIGIBLE',
|
|
969
|
-
'SESSION_EXPIRED',
|
|
970
|
-
'INVALID_SESSION'
|
|
1274
|
+
'UNPROCESSABLE_ENTITY', // 422 errors
|
|
1275
|
+
'USE_CASE_MISMATCH', // Use case validation errors
|
|
1276
|
+
'PHONE_NUMBER_MISMATCH', // Phone verification failures
|
|
1277
|
+
'VERIFICATION_FAILED', // Verification failures
|
|
1278
|
+
'INVALID_CREDENTIAL', // Bad credentials
|
|
1279
|
+
'CARRIER_NOT_ELIGIBLE', // Carrier not supported
|
|
1280
|
+
'SESSION_EXPIRED', // Session expired
|
|
1281
|
+
'INVALID_SESSION' // Invalid session
|
|
971
1282
|
];
|
|
972
1283
|
if (nonRetryableCodes.includes(error.code)) {
|
|
973
1284
|
return false;
|
|
974
1285
|
}
|
|
1286
|
+
// Only retry on network and server errors (5xx)
|
|
975
1287
|
const retryableCodes = [
|
|
976
1288
|
'NETWORK_ERROR',
|
|
977
1289
|
'REQUEST_TIMEOUT',
|
|
@@ -980,6 +1292,7 @@ export class PhoneAuthClient {
|
|
|
980
1292
|
'BAD_GATEWAY',
|
|
981
1293
|
'INTERNAL_SERVER_ERROR'
|
|
982
1294
|
];
|
|
1295
|
+
// Also check for cross-device error types in details
|
|
983
1296
|
const isCrossDeviceError = ((_a = error.details) === null || _a === void 0 ? void 0 : _a.errorType) && [
|
|
984
1297
|
'CROSS_DEVICE_TIMEOUT',
|
|
985
1298
|
'CROSS_DEVICE_CONNECTION_LOST',
|
|
@@ -989,13 +1302,17 @@ export class PhoneAuthClient {
|
|
|
989
1302
|
isCrossDeviceError ||
|
|
990
1303
|
(((_b = error.browserError) === null || _b === void 0 ? void 0 : _b.name) === 'NetworkError' && ((_c = error.browserError) === null || _c === void 0 ? void 0 : _c.code) !== 19);
|
|
991
1304
|
}
|
|
1305
|
+
/**
|
|
1306
|
+
* Analyze and enhance errors specific to cross-device flows
|
|
1307
|
+
*/
|
|
992
1308
|
analyzeCrossDeviceError(error, prepareResponse) {
|
|
993
1309
|
var _a;
|
|
994
1310
|
const errorObj = error;
|
|
1311
|
+
// Check for specific cross-device error patterns
|
|
995
1312
|
const isCrossDeviceTimeout = (errorObj.name === 'AbortError' && this.crossDeviceActive) ||
|
|
996
1313
|
(((_a = errorObj.message) === null || _a === void 0 ? void 0 : _a.includes('timeout')) && this.crossDeviceActive);
|
|
997
1314
|
const isCrossDeviceNetworkIssue = errorObj.name === 'NetworkError' &&
|
|
998
|
-
errorObj.code !== 19 &&
|
|
1315
|
+
errorObj.code !== 19 && // Not user cancellation
|
|
999
1316
|
this.crossDeviceActive;
|
|
1000
1317
|
if (isCrossDeviceTimeout) {
|
|
1001
1318
|
return {
|
|
@@ -1023,8 +1340,12 @@ export class PhoneAuthClient {
|
|
|
1023
1340
|
browserError: error.browserError
|
|
1024
1341
|
};
|
|
1025
1342
|
}
|
|
1343
|
+
// Return the original error if not cross-device specific
|
|
1026
1344
|
return error;
|
|
1027
1345
|
}
|
|
1346
|
+
/**
|
|
1347
|
+
* Cache successful session for retry scenarios
|
|
1348
|
+
*/
|
|
1028
1349
|
cacheSession(options, result) {
|
|
1029
1350
|
const cacheKey = this.getCacheKey(options);
|
|
1030
1351
|
this.sessionCache.set(cacheKey, {
|
|
@@ -1032,11 +1353,15 @@ export class PhoneAuthClient {
|
|
|
1032
1353
|
data: result
|
|
1033
1354
|
});
|
|
1034
1355
|
}
|
|
1356
|
+
/**
|
|
1357
|
+
* Retrieve cached session if available and recent
|
|
1358
|
+
*/
|
|
1035
1359
|
getCachedSession(options) {
|
|
1036
1360
|
const cacheKey = this.getCacheKey(options);
|
|
1037
1361
|
const cached = this.sessionCache.get(cacheKey);
|
|
1038
1362
|
if (!cached)
|
|
1039
1363
|
return null;
|
|
1364
|
+
// Cache valid for 5 minutes
|
|
1040
1365
|
const cacheValidMs = 5 * 60 * 1000;
|
|
1041
1366
|
if (Date.now() - cached.timestamp > cacheValidMs) {
|
|
1042
1367
|
this.sessionCache.delete(cacheKey);
|
|
@@ -1044,14 +1369,22 @@ export class PhoneAuthClient {
|
|
|
1044
1369
|
}
|
|
1045
1370
|
return cached.data;
|
|
1046
1371
|
}
|
|
1372
|
+
/**
|
|
1373
|
+
* Generate cache key for session storage
|
|
1374
|
+
*/
|
|
1047
1375
|
getCacheKey(options) {
|
|
1048
1376
|
var _a, _b;
|
|
1049
1377
|
return `${options.use_case}-${options.phone_number || 'no-phone'}-${((_a = options.plmn) === null || _a === void 0 ? void 0 : _a.mcc) || ''}-${((_b = options.plmn) === null || _b === void 0 ? void 0 : _b.mnc) || ''}`;
|
|
1050
1378
|
}
|
|
1379
|
+
/**
|
|
1380
|
+
* Set up periodic cache cleanup
|
|
1381
|
+
*/
|
|
1051
1382
|
setupCacheCleanup() {
|
|
1383
|
+
// Only run cleanup in browser environment (not during SSR)
|
|
1052
1384
|
if (typeof window === 'undefined') {
|
|
1053
1385
|
return;
|
|
1054
1386
|
}
|
|
1387
|
+
// Clean up expired cache entries every minute
|
|
1055
1388
|
setInterval(() => {
|
|
1056
1389
|
const now = Date.now();
|
|
1057
1390
|
const cacheValidMs = 5 * 60 * 1000;
|
|
@@ -1062,6 +1395,9 @@ export class PhoneAuthClient {
|
|
|
1062
1395
|
}
|
|
1063
1396
|
}, 60000);
|
|
1064
1397
|
}
|
|
1398
|
+
/**
|
|
1399
|
+
* Utility delay function
|
|
1400
|
+
*/
|
|
1065
1401
|
delay(ms) {
|
|
1066
1402
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
1067
1403
|
}
|