@sparkvault/sdk 1.0.0 → 1.1.6
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 +536 -502
- package/dist/identity/api.d.ts +4 -3
- package/dist/identity/renderer.d.ts +9 -2
- package/dist/identity/types.d.ts +2 -0
- package/dist/identity/views/icons.d.ts +4 -0
- package/dist/identity/views/sparklink-waiting.d.ts +7 -0
- package/dist/sparkvault.cjs.js +198 -49
- package/dist/sparkvault.cjs.js.map +1 -1
- package/dist/sparkvault.esm.js +198 -49
- package/dist/sparkvault.esm.js.map +1 -1
- package/dist/sparkvault.js +1 -1
- package/dist/sparkvault.js.map +1 -1
- package/package.json +1 -1
package/dist/sparkvault.esm.js
CHANGED
|
@@ -726,7 +726,18 @@ class IdentityApi {
|
|
|
726
726
|
* Start passkey registration
|
|
727
727
|
*/
|
|
728
728
|
async startPasskeyRegister(email) {
|
|
729
|
-
|
|
729
|
+
// Backend returns { options: PublicKeyCredentialCreationOptions, session: {...} }
|
|
730
|
+
// Extract and flatten to match PasskeyChallengeResponse
|
|
731
|
+
const response = await this.request('POST', '/passkey/register', { email });
|
|
732
|
+
return {
|
|
733
|
+
challenge: response.options.challenge,
|
|
734
|
+
rpId: response.options.rp.id,
|
|
735
|
+
rpName: response.options.rp.name,
|
|
736
|
+
userId: response.options.user.id,
|
|
737
|
+
userName: response.options.user.name,
|
|
738
|
+
timeout: response.options.timeout,
|
|
739
|
+
session: response.session,
|
|
740
|
+
};
|
|
730
741
|
}
|
|
731
742
|
/**
|
|
732
743
|
* Complete passkey registration
|
|
@@ -734,7 +745,7 @@ class IdentityApi {
|
|
|
734
745
|
async completePasskeyRegister(params) {
|
|
735
746
|
const attestation = params.credential.response;
|
|
736
747
|
return this.request('POST', '/passkey/register/complete', {
|
|
737
|
-
|
|
748
|
+
session: params.session,
|
|
738
749
|
credential: {
|
|
739
750
|
id: params.credential.id,
|
|
740
751
|
rawId: arrayBufferToBase64url(params.credential.rawId),
|
|
@@ -750,7 +761,17 @@ class IdentityApi {
|
|
|
750
761
|
* Start passkey verification
|
|
751
762
|
*/
|
|
752
763
|
async startPasskeyVerify(email) {
|
|
753
|
-
|
|
764
|
+
// Backend returns { options: PublicKeyCredentialRequestOptions, session: {...} }
|
|
765
|
+
// Extract and flatten to match PasskeyChallengeResponse
|
|
766
|
+
const response = await this.request('POST', '/passkey/verify', { email });
|
|
767
|
+
return {
|
|
768
|
+
challenge: response.options.challenge,
|
|
769
|
+
rpId: response.options.rpId,
|
|
770
|
+
rpName: 'SparkVault Identity', // Not returned by verify endpoint
|
|
771
|
+
timeout: response.options.timeout,
|
|
772
|
+
allowCredentials: response.options.allowCredentials,
|
|
773
|
+
session: response.session,
|
|
774
|
+
};
|
|
754
775
|
}
|
|
755
776
|
/**
|
|
756
777
|
* Complete passkey verification
|
|
@@ -758,7 +779,7 @@ class IdentityApi {
|
|
|
758
779
|
async completePasskeyVerify(params) {
|
|
759
780
|
const assertion = params.credential.response;
|
|
760
781
|
return this.request('POST', '/passkey/verify/complete', {
|
|
761
|
-
|
|
782
|
+
session: params.session,
|
|
762
783
|
credential: {
|
|
763
784
|
id: params.credential.id,
|
|
764
785
|
rawId: arrayBufferToBase64url(params.credential.rawId),
|
|
@@ -795,12 +816,16 @@ class IdentityApi {
|
|
|
795
816
|
return `${this.baseUrl}/saml/${provider}?${params}`;
|
|
796
817
|
}
|
|
797
818
|
/**
|
|
798
|
-
* Send SparkLink email for identity verification
|
|
819
|
+
* Send SparkLink email for identity verification.
|
|
820
|
+
* Includes openerOrigin for postMessage-based completion notification.
|
|
799
821
|
*/
|
|
800
822
|
async sendSparkLink(email) {
|
|
801
823
|
return this.request('POST', '/sparklink/send', {
|
|
802
824
|
email,
|
|
803
825
|
type: 'verify_identity',
|
|
826
|
+
// Send opener origin for postMessage on verification completion
|
|
827
|
+
// This allows the ceremony page to notify the SDK directly instead of polling
|
|
828
|
+
openerOrigin: typeof window !== 'undefined' ? window.location.origin : undefined,
|
|
804
829
|
});
|
|
805
830
|
}
|
|
806
831
|
/**
|
|
@@ -1243,15 +1268,15 @@ class PasskeyHandler {
|
|
|
1243
1268
|
*/
|
|
1244
1269
|
async register() {
|
|
1245
1270
|
try {
|
|
1246
|
-
const
|
|
1271
|
+
const challengeResponse = await this.api.startPasskeyRegister(this.state.recipient);
|
|
1247
1272
|
const publicKeyOptions = {
|
|
1248
|
-
challenge: base64urlToArrayBuffer(
|
|
1273
|
+
challenge: base64urlToArrayBuffer(challengeResponse.challenge),
|
|
1249
1274
|
rp: {
|
|
1250
|
-
id:
|
|
1251
|
-
name:
|
|
1275
|
+
id: challengeResponse.rpId,
|
|
1276
|
+
name: challengeResponse.rpName,
|
|
1252
1277
|
},
|
|
1253
1278
|
user: {
|
|
1254
|
-
id: base64urlToArrayBuffer(
|
|
1279
|
+
id: base64urlToArrayBuffer(challengeResponse.userId ?? this.state.recipient),
|
|
1255
1280
|
name: this.state.recipient,
|
|
1256
1281
|
displayName: this.state.recipient,
|
|
1257
1282
|
},
|
|
@@ -1259,7 +1284,7 @@ class PasskeyHandler {
|
|
|
1259
1284
|
{ type: 'public-key', alg: -7 }, // ES256
|
|
1260
1285
|
{ type: 'public-key', alg: -257 }, // RS256
|
|
1261
1286
|
],
|
|
1262
|
-
timeout:
|
|
1287
|
+
timeout: challengeResponse.timeout,
|
|
1263
1288
|
authenticatorSelection: {
|
|
1264
1289
|
residentKey: 'preferred',
|
|
1265
1290
|
userVerification: 'preferred',
|
|
@@ -1277,7 +1302,7 @@ class PasskeyHandler {
|
|
|
1277
1302
|
};
|
|
1278
1303
|
}
|
|
1279
1304
|
const response = await this.api.completePasskeyRegister({
|
|
1280
|
-
|
|
1305
|
+
session: challengeResponse.session,
|
|
1281
1306
|
credential,
|
|
1282
1307
|
});
|
|
1283
1308
|
// Update state - user now has a passkey
|
|
@@ -1300,13 +1325,13 @@ class PasskeyHandler {
|
|
|
1300
1325
|
*/
|
|
1301
1326
|
async verify() {
|
|
1302
1327
|
try {
|
|
1303
|
-
const
|
|
1328
|
+
const challengeResponse = await this.api.startPasskeyVerify(this.state.recipient);
|
|
1304
1329
|
const publicKeyOptions = {
|
|
1305
|
-
challenge: base64urlToArrayBuffer(
|
|
1306
|
-
rpId:
|
|
1307
|
-
timeout:
|
|
1330
|
+
challenge: base64urlToArrayBuffer(challengeResponse.challenge),
|
|
1331
|
+
rpId: challengeResponse.rpId,
|
|
1332
|
+
timeout: challengeResponse.timeout,
|
|
1308
1333
|
userVerification: 'preferred',
|
|
1309
|
-
allowCredentials:
|
|
1334
|
+
allowCredentials: challengeResponse.allowCredentials?.map((cred) => ({
|
|
1310
1335
|
id: base64urlToArrayBuffer(cred.id),
|
|
1311
1336
|
type: cred.type,
|
|
1312
1337
|
transports: ['internal', 'hybrid', 'usb', 'ble', 'nfc'],
|
|
@@ -1323,7 +1348,7 @@ class PasskeyHandler {
|
|
|
1323
1348
|
};
|
|
1324
1349
|
}
|
|
1325
1350
|
const response = await this.api.completePasskeyVerify({
|
|
1326
|
-
|
|
1351
|
+
session: challengeResponse.session,
|
|
1327
1352
|
credential,
|
|
1328
1353
|
});
|
|
1329
1354
|
return {
|
|
@@ -1731,7 +1756,7 @@ function getStyles(options) {
|
|
|
1731
1756
|
======================================== */
|
|
1732
1757
|
|
|
1733
1758
|
.sv-body {
|
|
1734
|
-
padding:
|
|
1759
|
+
padding: 24px;
|
|
1735
1760
|
overflow-y: auto;
|
|
1736
1761
|
flex: 1;
|
|
1737
1762
|
}
|
|
@@ -2464,6 +2489,36 @@ function getStyles(options) {
|
|
|
2464
2489
|
color: ${tokens.textSecondary};
|
|
2465
2490
|
}
|
|
2466
2491
|
|
|
2492
|
+
/* SparkLink Expired State */
|
|
2493
|
+
.sv-sparklink-expired {
|
|
2494
|
+
flex-direction: column;
|
|
2495
|
+
gap: 12px;
|
|
2496
|
+
background: ${tokens.bgSubtle};
|
|
2497
|
+
border: 1px solid ${tokens.border};
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
.sv-sparklink-expired-icon {
|
|
2501
|
+
display: flex;
|
|
2502
|
+
justify-content: center;
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
.sv-sparklink-expired-icon svg {
|
|
2506
|
+
width: 48px;
|
|
2507
|
+
height: 48px;
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
.sv-sparklink-expired-text {
|
|
2511
|
+
font-size: 14px;
|
|
2512
|
+
font-weight: 500;
|
|
2513
|
+
color: ${tokens.textSecondary};
|
|
2514
|
+
text-align: center;
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
.sv-expired-icon {
|
|
2518
|
+
width: 48px;
|
|
2519
|
+
height: 48px;
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2467
2522
|
/* ========================================
|
|
2468
2523
|
DIVIDER
|
|
2469
2524
|
======================================== */
|
|
@@ -2511,6 +2566,7 @@ function getStyles(options) {
|
|
|
2511
2566
|
.sv-inline-container {
|
|
2512
2567
|
display: flex;
|
|
2513
2568
|
flex-direction: column;
|
|
2569
|
+
height: 100%;
|
|
2514
2570
|
background: ${tokens.bg};
|
|
2515
2571
|
color: ${tokens.textPrimary};
|
|
2516
2572
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
@@ -2526,7 +2582,11 @@ function getStyles(options) {
|
|
|
2526
2582
|
}
|
|
2527
2583
|
|
|
2528
2584
|
.sv-inline-body {
|
|
2585
|
+
display: flex;
|
|
2586
|
+
flex-direction: column;
|
|
2587
|
+
justify-content: center;
|
|
2529
2588
|
min-height: 200px;
|
|
2589
|
+
flex: 1;
|
|
2530
2590
|
}
|
|
2531
2591
|
|
|
2532
2592
|
/* ========================================
|
|
@@ -2545,7 +2605,7 @@ function getStyles(options) {
|
|
|
2545
2605
|
}
|
|
2546
2606
|
|
|
2547
2607
|
.sv-body {
|
|
2548
|
-
padding:
|
|
2608
|
+
padding: 24px;
|
|
2549
2609
|
}
|
|
2550
2610
|
|
|
2551
2611
|
.sv-totp-digit {
|
|
@@ -2689,6 +2749,32 @@ function createErrorIcon() {
|
|
|
2689
2749
|
svg.appendChild(group);
|
|
2690
2750
|
return svg;
|
|
2691
2751
|
}
|
|
2752
|
+
/**
|
|
2753
|
+
* Expired icon - grayscale triangle alert for expired states
|
|
2754
|
+
*/
|
|
2755
|
+
function createExpiredIcon() {
|
|
2756
|
+
const svg = createSvgElement('0 0 48 48', 48, 48);
|
|
2757
|
+
svg.classList.add('sv-expired-icon');
|
|
2758
|
+
// Circle background - muted gray
|
|
2759
|
+
svg.appendChild(createCircle(24, 24, 22, {
|
|
2760
|
+
fill: '#F5F5F5',
|
|
2761
|
+
stroke: '#E5E5E5',
|
|
2762
|
+
'stroke-width': '1',
|
|
2763
|
+
}));
|
|
2764
|
+
// Alert triangle - gray
|
|
2765
|
+
const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
2766
|
+
group.setAttribute('transform', 'translate(10, 12)');
|
|
2767
|
+
group.appendChild(createPath('M14 4L2 24h24L14 4z', { fill: '#737373' }));
|
|
2768
|
+
// Exclamation
|
|
2769
|
+
group.appendChild(createPath('M14 10v5', {
|
|
2770
|
+
stroke: '#FFFFFF',
|
|
2771
|
+
'stroke-width': '2',
|
|
2772
|
+
'stroke-linecap': 'round',
|
|
2773
|
+
}));
|
|
2774
|
+
group.appendChild(createCircle(14, 19, 1, { fill: '#FFFFFF' }));
|
|
2775
|
+
svg.appendChild(group);
|
|
2776
|
+
return svg;
|
|
2777
|
+
}
|
|
2692
2778
|
/**
|
|
2693
2779
|
* Method icons
|
|
2694
2780
|
*/
|
|
@@ -4021,6 +4107,8 @@ class SparkLinkWaitingView {
|
|
|
4021
4107
|
this.expirationTimer = null;
|
|
4022
4108
|
this.resendTimer = null;
|
|
4023
4109
|
this.countdownElement = null;
|
|
4110
|
+
this.countdownSection = null;
|
|
4111
|
+
this.waitingSection = null;
|
|
4024
4112
|
this.resendButton = null;
|
|
4025
4113
|
this.backLink = null;
|
|
4026
4114
|
this.fallbackButton = null;
|
|
@@ -4039,7 +4127,7 @@ class SparkLinkWaitingView {
|
|
|
4039
4127
|
subtitle.className = 'sv-subtitle';
|
|
4040
4128
|
subtitle.innerHTML = `We sent a secure link to<br><strong>${escapeHtml(this.props.email)}</strong>`;
|
|
4041
4129
|
// Spinner with message
|
|
4042
|
-
|
|
4130
|
+
this.waitingSection = div('sv-sparklink-waiting');
|
|
4043
4131
|
const spinner = document.createElement('div');
|
|
4044
4132
|
spinner.className = 'sv-spinner sv-spinner-small';
|
|
4045
4133
|
spinner.setAttribute('role', 'status');
|
|
@@ -4047,17 +4135,17 @@ class SparkLinkWaitingView {
|
|
|
4047
4135
|
const waitingText = document.createElement('span');
|
|
4048
4136
|
waitingText.className = 'sv-sparklink-waiting-text';
|
|
4049
4137
|
waitingText.textContent = 'Waiting for you to click the link...';
|
|
4050
|
-
waitingSection.appendChild(spinner);
|
|
4051
|
-
waitingSection.appendChild(waitingText);
|
|
4138
|
+
this.waitingSection.appendChild(spinner);
|
|
4139
|
+
this.waitingSection.appendChild(waitingText);
|
|
4052
4140
|
// Countdown timer
|
|
4053
|
-
|
|
4141
|
+
this.countdownSection = div('sv-sparklink-countdown');
|
|
4054
4142
|
const countdownLabel = document.createElement('span');
|
|
4055
4143
|
countdownLabel.textContent = 'Link expires in ';
|
|
4056
4144
|
this.countdownElement = document.createElement('span');
|
|
4057
4145
|
this.countdownElement.className = 'sv-sparklink-countdown-time';
|
|
4058
4146
|
this.startExpirationTimer();
|
|
4059
|
-
countdownSection.appendChild(countdownLabel);
|
|
4060
|
-
countdownSection.appendChild(this.countdownElement);
|
|
4147
|
+
this.countdownSection.appendChild(countdownLabel);
|
|
4148
|
+
this.countdownSection.appendChild(this.countdownElement);
|
|
4061
4149
|
// Resend button
|
|
4062
4150
|
const resendContainer = div('sv-resend-container');
|
|
4063
4151
|
this.resendButton = document.createElement('button');
|
|
@@ -4075,8 +4163,8 @@ class SparkLinkWaitingView {
|
|
|
4075
4163
|
container.appendChild(this.backLink);
|
|
4076
4164
|
container.appendChild(title);
|
|
4077
4165
|
container.appendChild(subtitle);
|
|
4078
|
-
container.appendChild(waitingSection);
|
|
4079
|
-
container.appendChild(countdownSection);
|
|
4166
|
+
container.appendChild(this.waitingSection);
|
|
4167
|
+
container.appendChild(this.countdownSection);
|
|
4080
4168
|
container.appendChild(resendContainer);
|
|
4081
4169
|
container.appendChild(fallbackContainer);
|
|
4082
4170
|
return container;
|
|
@@ -4118,13 +4206,36 @@ class SparkLinkWaitingView {
|
|
|
4118
4206
|
this.countdownElement.textContent = this.formatTime(secondsRemaining);
|
|
4119
4207
|
},
|
|
4120
4208
|
onExpired: () => {
|
|
4121
|
-
|
|
4122
|
-
return;
|
|
4123
|
-
this.countdownElement.textContent = 'Expired';
|
|
4209
|
+
this.showExpiredState();
|
|
4124
4210
|
},
|
|
4125
4211
|
});
|
|
4126
4212
|
this.expirationTimer.start();
|
|
4127
4213
|
}
|
|
4214
|
+
/**
|
|
4215
|
+
* Switch to expired state - static icon, no animation, helpful message
|
|
4216
|
+
*/
|
|
4217
|
+
showExpiredState() {
|
|
4218
|
+
// Notify renderer to stop polling
|
|
4219
|
+
this.props.onExpired();
|
|
4220
|
+
// Update waiting section: replace spinner with expired icon
|
|
4221
|
+
if (this.waitingSection) {
|
|
4222
|
+
this.waitingSection.innerHTML = '';
|
|
4223
|
+
this.waitingSection.classList.add('sv-sparklink-expired');
|
|
4224
|
+
// Add expired icon
|
|
4225
|
+
const iconContainer = div('sv-sparklink-expired-icon');
|
|
4226
|
+
iconContainer.appendChild(createExpiredIcon());
|
|
4227
|
+
this.waitingSection.appendChild(iconContainer);
|
|
4228
|
+
// Add expired message
|
|
4229
|
+
const expiredText = document.createElement('span');
|
|
4230
|
+
expiredText.className = 'sv-sparklink-expired-text';
|
|
4231
|
+
expiredText.textContent = 'This link has expired';
|
|
4232
|
+
this.waitingSection.appendChild(expiredText);
|
|
4233
|
+
}
|
|
4234
|
+
// Hide countdown section
|
|
4235
|
+
if (this.countdownSection) {
|
|
4236
|
+
this.countdownSection.style.display = 'none';
|
|
4237
|
+
}
|
|
4238
|
+
}
|
|
4128
4239
|
handleResend() {
|
|
4129
4240
|
if (this.resendTimer?.isRunning() || !this.resendButton)
|
|
4130
4241
|
return;
|
|
@@ -4251,8 +4362,9 @@ class IdentityRenderer {
|
|
|
4251
4362
|
// View management
|
|
4252
4363
|
this.currentView = null;
|
|
4253
4364
|
this.focusTimeoutId = null;
|
|
4254
|
-
// SparkLink polling
|
|
4365
|
+
// SparkLink polling and postMessage listener
|
|
4255
4366
|
this.pollingInterval = null;
|
|
4367
|
+
this.messageListener = null;
|
|
4256
4368
|
this.container = container;
|
|
4257
4369
|
this.api = api;
|
|
4258
4370
|
this.options = options;
|
|
@@ -4374,7 +4486,11 @@ class IdentityRenderer {
|
|
|
4374
4486
|
this.currentView = view;
|
|
4375
4487
|
clearChildren(body);
|
|
4376
4488
|
body.appendChild(view.render());
|
|
4377
|
-
|
|
4489
|
+
// Auto-focus: prefer inputs first, then primary/method buttons (skip back links)
|
|
4490
|
+
const focusable = body.querySelector('input:not([disabled]), ' +
|
|
4491
|
+
'button.sv-btn-primary:not([disabled]), ' +
|
|
4492
|
+
'button.sv-btn-email-primary:not([disabled]), ' +
|
|
4493
|
+
'button.sv-btn-method:not([disabled])');
|
|
4378
4494
|
if (focusable) {
|
|
4379
4495
|
// Clear any previous focus timeout to avoid stacking
|
|
4380
4496
|
if (this.focusTimeoutId !== null) {
|
|
@@ -4447,6 +4563,7 @@ class IdentityRenderer {
|
|
|
4447
4563
|
onResend: () => this.handleSparkLinkResend(),
|
|
4448
4564
|
onFallback: () => this.handleSparkLinkFallback(),
|
|
4449
4565
|
onBack: () => this.showMethodSelect(),
|
|
4566
|
+
onExpired: () => this.stopSparkLinkPolling(),
|
|
4450
4567
|
});
|
|
4451
4568
|
case 'oauth-pending':
|
|
4452
4569
|
return new LoadingView({ message: `Connecting to ${state.provider}...` });
|
|
@@ -4789,41 +4906,73 @@ class IdentityRenderer {
|
|
|
4789
4906
|
this.callbacks.onSuccess(pendingResult);
|
|
4790
4907
|
}
|
|
4791
4908
|
/**
|
|
4792
|
-
* Start
|
|
4909
|
+
* Start listening for SparkLink verification completion.
|
|
4910
|
+
* Uses postMessage as primary mechanism (instant notification from ceremony page),
|
|
4911
|
+
* with polling as fallback for edge cases where postMessage might fail.
|
|
4793
4912
|
*/
|
|
4794
4913
|
startSparkLinkPolling(sparkId) {
|
|
4795
4914
|
this.stopSparkLinkPolling();
|
|
4915
|
+
// Primary: Listen for postMessage from ceremony page
|
|
4916
|
+
// This is faster and more reliable than polling
|
|
4917
|
+
this.messageListener = (event) => {
|
|
4918
|
+
// Validate message structure and type
|
|
4919
|
+
if (!event.data || typeof event.data !== 'object')
|
|
4920
|
+
return;
|
|
4921
|
+
if (event.data.type !== 'sparklink_verified')
|
|
4922
|
+
return;
|
|
4923
|
+
const { token, identity, identityType } = event.data;
|
|
4924
|
+
// Validate required fields
|
|
4925
|
+
if (!token || !identity)
|
|
4926
|
+
return;
|
|
4927
|
+
this.handleSparkLinkVerified({
|
|
4928
|
+
token,
|
|
4929
|
+
identity,
|
|
4930
|
+
identityType: identityType || 'email',
|
|
4931
|
+
});
|
|
4932
|
+
};
|
|
4933
|
+
window.addEventListener('message', this.messageListener);
|
|
4934
|
+
// Fallback: Poll status endpoint every 2 seconds
|
|
4935
|
+
// This catches cases where postMessage might not work (popup blockers, etc)
|
|
4796
4936
|
this.pollingInterval = setInterval(async () => {
|
|
4797
4937
|
const status = await this.sparkLinkHandler.checkStatus(sparkId);
|
|
4798
4938
|
if (status.verified && status.token && status.identity) {
|
|
4799
|
-
this.
|
|
4800
|
-
const result = {
|
|
4939
|
+
this.handleSparkLinkVerified({
|
|
4801
4940
|
token: status.token,
|
|
4802
4941
|
identity: status.identity,
|
|
4803
4942
|
identityType: status.identityType || 'email',
|
|
4804
|
-
};
|
|
4805
|
-
// Check if we should prompt for passkey registration
|
|
4806
|
-
if (await this.shouldShowPasskeyPrompt()) {
|
|
4807
|
-
this.setState({
|
|
4808
|
-
view: 'passkey-prompt',
|
|
4809
|
-
email: this.recipient,
|
|
4810
|
-
pendingResult: result,
|
|
4811
|
-
});
|
|
4812
|
-
return;
|
|
4813
|
-
}
|
|
4814
|
-
this.close();
|
|
4815
|
-
this.callbacks.onSuccess(result);
|
|
4943
|
+
});
|
|
4816
4944
|
}
|
|
4817
4945
|
}, 2000);
|
|
4818
4946
|
}
|
|
4819
4947
|
/**
|
|
4820
|
-
*
|
|
4948
|
+
* Handle successful SparkLink verification (from either postMessage or polling).
|
|
4949
|
+
*/
|
|
4950
|
+
async handleSparkLinkVerified(result) {
|
|
4951
|
+
this.stopSparkLinkPolling();
|
|
4952
|
+
// Check if we should prompt for passkey registration
|
|
4953
|
+
if (await this.shouldShowPasskeyPrompt()) {
|
|
4954
|
+
this.setState({
|
|
4955
|
+
view: 'passkey-prompt',
|
|
4956
|
+
email: this.recipient,
|
|
4957
|
+
pendingResult: result,
|
|
4958
|
+
});
|
|
4959
|
+
return;
|
|
4960
|
+
}
|
|
4961
|
+
this.close();
|
|
4962
|
+
this.callbacks.onSuccess(result);
|
|
4963
|
+
}
|
|
4964
|
+
/**
|
|
4965
|
+
* Stop SparkLink polling and message listener.
|
|
4821
4966
|
*/
|
|
4822
4967
|
stopSparkLinkPolling() {
|
|
4823
4968
|
if (this.pollingInterval) {
|
|
4824
4969
|
clearInterval(this.pollingInterval);
|
|
4825
4970
|
this.pollingInterval = null;
|
|
4826
4971
|
}
|
|
4972
|
+
if (this.messageListener) {
|
|
4973
|
+
window.removeEventListener('message', this.messageListener);
|
|
4974
|
+
this.messageListener = null;
|
|
4975
|
+
}
|
|
4827
4976
|
}
|
|
4828
4977
|
/**
|
|
4829
4978
|
* Handle resending SparkLink.
|