@swype-org/react-sdk 0.1.86 → 0.1.88
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/dist/index.cjs +45 -74
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -23
- package/dist/index.d.ts +20 -23
- package/dist/index.js +45 -74
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -189,11 +189,13 @@ interface UserConfig {
|
|
|
189
189
|
passkey?: {
|
|
190
190
|
credentialId: string;
|
|
191
191
|
publicKey: string;
|
|
192
|
+
lastVerificationToken?: string | null;
|
|
192
193
|
} | null;
|
|
193
194
|
/** All registered WebAuthn passkey credentials for this user */
|
|
194
195
|
passkeys?: {
|
|
195
196
|
credentialId: string;
|
|
196
197
|
publicKey: string;
|
|
198
|
+
lastVerificationToken?: string | null;
|
|
197
199
|
}[];
|
|
198
200
|
}
|
|
199
201
|
/** Theme mode */
|
|
@@ -389,19 +391,16 @@ interface SwypePaymentProps {
|
|
|
389
391
|
declare function SwypePayment(props: SwypePaymentProps): react_jsx_runtime.JSX.Element;
|
|
390
392
|
|
|
391
393
|
/**
|
|
392
|
-
* Cross-origin iframe passkey
|
|
394
|
+
* Cross-origin iframe passkey helpers.
|
|
393
395
|
*
|
|
394
396
|
* When the webview-app runs inside a cross-origin iframe, WebAuthn
|
|
395
397
|
* ceremonies cannot execute from within the iframe on Safari. For passkey
|
|
396
398
|
* creation, we first attempt a direct `navigator.credentials.create()`
|
|
397
399
|
* call (works in Chrome/Firefox with the iframe permissions policy). If
|
|
398
400
|
* that fails (Safari), we open a same-origin pop-up window on the Swype
|
|
399
|
-
* domain to perform the ceremony
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
-
* Passkey *assertion* (`navigator.credentials.get`) still delegates to
|
|
404
|
-
* the parent page via postMessage as before.
|
|
401
|
+
* domain to perform the ceremony. The popup writes the result to the
|
|
402
|
+
* server with a verification token, and the opener reads from the server
|
|
403
|
+
* after the popup closes.
|
|
405
404
|
*/
|
|
406
405
|
/**
|
|
407
406
|
* Thrown when `navigator.credentials.create()` fails inside a
|
|
@@ -429,7 +428,7 @@ interface PasskeyPopupOptions {
|
|
|
429
428
|
};
|
|
430
429
|
timeout?: number;
|
|
431
430
|
/** Populated by `createPasskeyViaPopup`; not set by callers. */
|
|
432
|
-
|
|
431
|
+
verificationToken?: string;
|
|
433
432
|
/** Privy JWT so the popup can register the passkey server-side. */
|
|
434
433
|
authToken?: string;
|
|
435
434
|
/** Core API base URL for server-side passkey registration. */
|
|
@@ -440,24 +439,14 @@ interface PasskeyPopupOptions {
|
|
|
440
439
|
* passkey creation. Used as a fallback when Safari blocks
|
|
441
440
|
* `navigator.credentials.create()` inside a cross-origin iframe.
|
|
442
441
|
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
* opener reference.
|
|
447
|
-
*
|
|
448
|
-
* When both client-side channels are blocked (Safari ITP partitions
|
|
449
|
-
* BroadcastChannel by top-level origin), the popup registers the
|
|
450
|
-
* passkey directly with the backend. On popup close, this function
|
|
451
|
-
* checks the server for newly registered passkeys as a final fallback.
|
|
442
|
+
* The popup registers the passkey with the server using a verification
|
|
443
|
+
* token. After the popup closes, this function checks the server for
|
|
444
|
+
* the passkey matching the token.
|
|
452
445
|
*
|
|
453
446
|
* Must be called from a user-gesture handler (e.g. button click) to
|
|
454
447
|
* avoid the browser's pop-up blocker.
|
|
455
|
-
*
|
|
456
|
-
* @param existingCredentialIds - Credential IDs known before the popup
|
|
457
|
-
* opens. Used to diff against server state when the popup closes
|
|
458
|
-
* without delivering a client-side result.
|
|
459
448
|
*/
|
|
460
|
-
declare function createPasskeyViaPopup(options: PasskeyPopupOptions
|
|
449
|
+
declare function createPasskeyViaPopup(options: PasskeyPopupOptions): Promise<{
|
|
461
450
|
credentialId: string;
|
|
462
451
|
publicKey: string;
|
|
463
452
|
}>;
|
|
@@ -465,7 +454,11 @@ interface PasskeyVerifyPopupOptions {
|
|
|
465
454
|
credentialIds: string[];
|
|
466
455
|
rpId: string;
|
|
467
456
|
/** Populated by `findDevicePasskeyViaPopup`; not set by callers. */
|
|
468
|
-
|
|
457
|
+
verificationToken?: string;
|
|
458
|
+
/** Privy JWT so the popup can report verification server-side. */
|
|
459
|
+
authToken?: string;
|
|
460
|
+
/** Core API base URL for server-side passkey activity reporting. */
|
|
461
|
+
apiBaseUrl?: string;
|
|
469
462
|
}
|
|
470
463
|
/**
|
|
471
464
|
* Opens a same-origin pop-up window on the Swype domain to check whether
|
|
@@ -475,6 +468,10 @@ interface PasskeyVerifyPopupOptions {
|
|
|
475
468
|
* inside a cross-origin iframe. The popup runs on the Swype domain where
|
|
476
469
|
* the rpId matches, so WebAuthn works.
|
|
477
470
|
*
|
|
471
|
+
* The popup writes a verification token to the server. After the popup
|
|
472
|
+
* closes, the opener checks the server for the matching token to determine
|
|
473
|
+
* which credential was verified.
|
|
474
|
+
*
|
|
478
475
|
* Must be called from a user-gesture handler (e.g. button click) to
|
|
479
476
|
* avoid the browser's pop-up blocker.
|
|
480
477
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -189,11 +189,13 @@ interface UserConfig {
|
|
|
189
189
|
passkey?: {
|
|
190
190
|
credentialId: string;
|
|
191
191
|
publicKey: string;
|
|
192
|
+
lastVerificationToken?: string | null;
|
|
192
193
|
} | null;
|
|
193
194
|
/** All registered WebAuthn passkey credentials for this user */
|
|
194
195
|
passkeys?: {
|
|
195
196
|
credentialId: string;
|
|
196
197
|
publicKey: string;
|
|
198
|
+
lastVerificationToken?: string | null;
|
|
197
199
|
}[];
|
|
198
200
|
}
|
|
199
201
|
/** Theme mode */
|
|
@@ -389,19 +391,16 @@ interface SwypePaymentProps {
|
|
|
389
391
|
declare function SwypePayment(props: SwypePaymentProps): react_jsx_runtime.JSX.Element;
|
|
390
392
|
|
|
391
393
|
/**
|
|
392
|
-
* Cross-origin iframe passkey
|
|
394
|
+
* Cross-origin iframe passkey helpers.
|
|
393
395
|
*
|
|
394
396
|
* When the webview-app runs inside a cross-origin iframe, WebAuthn
|
|
395
397
|
* ceremonies cannot execute from within the iframe on Safari. For passkey
|
|
396
398
|
* creation, we first attempt a direct `navigator.credentials.create()`
|
|
397
399
|
* call (works in Chrome/Firefox with the iframe permissions policy). If
|
|
398
400
|
* that fails (Safari), we open a same-origin pop-up window on the Swype
|
|
399
|
-
* domain to perform the ceremony
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
-
* Passkey *assertion* (`navigator.credentials.get`) still delegates to
|
|
404
|
-
* the parent page via postMessage as before.
|
|
401
|
+
* domain to perform the ceremony. The popup writes the result to the
|
|
402
|
+
* server with a verification token, and the opener reads from the server
|
|
403
|
+
* after the popup closes.
|
|
405
404
|
*/
|
|
406
405
|
/**
|
|
407
406
|
* Thrown when `navigator.credentials.create()` fails inside a
|
|
@@ -429,7 +428,7 @@ interface PasskeyPopupOptions {
|
|
|
429
428
|
};
|
|
430
429
|
timeout?: number;
|
|
431
430
|
/** Populated by `createPasskeyViaPopup`; not set by callers. */
|
|
432
|
-
|
|
431
|
+
verificationToken?: string;
|
|
433
432
|
/** Privy JWT so the popup can register the passkey server-side. */
|
|
434
433
|
authToken?: string;
|
|
435
434
|
/** Core API base URL for server-side passkey registration. */
|
|
@@ -440,24 +439,14 @@ interface PasskeyPopupOptions {
|
|
|
440
439
|
* passkey creation. Used as a fallback when Safari blocks
|
|
441
440
|
* `navigator.credentials.create()` inside a cross-origin iframe.
|
|
442
441
|
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
* opener reference.
|
|
447
|
-
*
|
|
448
|
-
* When both client-side channels are blocked (Safari ITP partitions
|
|
449
|
-
* BroadcastChannel by top-level origin), the popup registers the
|
|
450
|
-
* passkey directly with the backend. On popup close, this function
|
|
451
|
-
* checks the server for newly registered passkeys as a final fallback.
|
|
442
|
+
* The popup registers the passkey with the server using a verification
|
|
443
|
+
* token. After the popup closes, this function checks the server for
|
|
444
|
+
* the passkey matching the token.
|
|
452
445
|
*
|
|
453
446
|
* Must be called from a user-gesture handler (e.g. button click) to
|
|
454
447
|
* avoid the browser's pop-up blocker.
|
|
455
|
-
*
|
|
456
|
-
* @param existingCredentialIds - Credential IDs known before the popup
|
|
457
|
-
* opens. Used to diff against server state when the popup closes
|
|
458
|
-
* without delivering a client-side result.
|
|
459
448
|
*/
|
|
460
|
-
declare function createPasskeyViaPopup(options: PasskeyPopupOptions
|
|
449
|
+
declare function createPasskeyViaPopup(options: PasskeyPopupOptions): Promise<{
|
|
461
450
|
credentialId: string;
|
|
462
451
|
publicKey: string;
|
|
463
452
|
}>;
|
|
@@ -465,7 +454,11 @@ interface PasskeyVerifyPopupOptions {
|
|
|
465
454
|
credentialIds: string[];
|
|
466
455
|
rpId: string;
|
|
467
456
|
/** Populated by `findDevicePasskeyViaPopup`; not set by callers. */
|
|
468
|
-
|
|
457
|
+
verificationToken?: string;
|
|
458
|
+
/** Privy JWT so the popup can report verification server-side. */
|
|
459
|
+
authToken?: string;
|
|
460
|
+
/** Core API base URL for server-side passkey activity reporting. */
|
|
461
|
+
apiBaseUrl?: string;
|
|
469
462
|
}
|
|
470
463
|
/**
|
|
471
464
|
* Opens a same-origin pop-up window on the Swype domain to check whether
|
|
@@ -475,6 +468,10 @@ interface PasskeyVerifyPopupOptions {
|
|
|
475
468
|
* inside a cross-origin iframe. The popup runs on the Swype domain where
|
|
476
469
|
* the rpId matches, so WebAuthn works.
|
|
477
470
|
*
|
|
471
|
+
* The popup writes a verification token to the server. After the popup
|
|
472
|
+
* closes, the opener checks the server for the matching token to determine
|
|
473
|
+
* which credential was verified.
|
|
474
|
+
*
|
|
478
475
|
* Must be called from a user-gesture handler (e.g. button click) to
|
|
479
476
|
* avoid the browser's pop-up blocker.
|
|
480
477
|
*
|
package/dist/index.js
CHANGED
|
@@ -725,10 +725,10 @@ function isSafari() {
|
|
|
725
725
|
var POPUP_RESULT_TIMEOUT_MS = 12e4;
|
|
726
726
|
var POPUP_CLOSED_POLL_MS = 500;
|
|
727
727
|
var POPUP_CLOSED_GRACE_MS = 1e3;
|
|
728
|
-
function createPasskeyViaPopup(options
|
|
728
|
+
function createPasskeyViaPopup(options) {
|
|
729
729
|
return new Promise((resolve, reject) => {
|
|
730
|
-
const
|
|
731
|
-
const payload = { ...options,
|
|
730
|
+
const verificationToken = crypto.randomUUID();
|
|
731
|
+
const payload = { ...options, verificationToken };
|
|
732
732
|
const encoded = btoa(JSON.stringify(payload));
|
|
733
733
|
const popupUrl = `${window.location.origin}/passkey-register#${encoded}`;
|
|
734
734
|
const popup = window.open(popupUrl, "swype-passkey");
|
|
@@ -737,22 +737,21 @@ function createPasskeyViaPopup(options, existingCredentialIds = []) {
|
|
|
737
737
|
return;
|
|
738
738
|
}
|
|
739
739
|
let settled = false;
|
|
740
|
-
const channel = typeof BroadcastChannel !== "undefined" ? new BroadcastChannel(channelId) : null;
|
|
741
740
|
const timer = setTimeout(() => {
|
|
742
741
|
cleanup();
|
|
743
742
|
reject(new Error("Passkey creation timed out. Please try again."));
|
|
744
743
|
}, POPUP_RESULT_TIMEOUT_MS);
|
|
745
|
-
let closedGraceTimer = null;
|
|
746
744
|
const closedPoll = setInterval(() => {
|
|
747
745
|
if (popup.closed) {
|
|
748
746
|
clearInterval(closedPoll);
|
|
749
|
-
|
|
747
|
+
setTimeout(() => {
|
|
750
748
|
if (!settled) {
|
|
749
|
+
settled = true;
|
|
751
750
|
cleanup();
|
|
752
|
-
|
|
751
|
+
checkServerForPasskeyByToken(
|
|
753
752
|
options.authToken,
|
|
754
753
|
options.apiBaseUrl,
|
|
755
|
-
|
|
754
|
+
verificationToken
|
|
756
755
|
).then((result) => {
|
|
757
756
|
if (result) {
|
|
758
757
|
resolve(result);
|
|
@@ -766,42 +765,20 @@ function createPasskeyViaPopup(options, existingCredentialIds = []) {
|
|
|
766
765
|
}, POPUP_CLOSED_GRACE_MS);
|
|
767
766
|
}
|
|
768
767
|
}, POPUP_CLOSED_POLL_MS);
|
|
769
|
-
function handleResult(data) {
|
|
770
|
-
if (settled) return;
|
|
771
|
-
if (!data || typeof data !== "object") return;
|
|
772
|
-
if (data.type !== "swype:passkey-popup-result") return;
|
|
773
|
-
settled = true;
|
|
774
|
-
cleanup();
|
|
775
|
-
if (data.error) {
|
|
776
|
-
reject(new Error(data.error));
|
|
777
|
-
} else if (data.result) {
|
|
778
|
-
resolve(data.result);
|
|
779
|
-
} else {
|
|
780
|
-
reject(new Error("Invalid passkey popup response."));
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
if (channel) {
|
|
784
|
-
channel.onmessage = (event) => handleResult(event.data);
|
|
785
|
-
}
|
|
786
|
-
const postMessageHandler = (event) => {
|
|
787
|
-
if (event.source !== popup) return;
|
|
788
|
-
handleResult(event.data);
|
|
789
|
-
};
|
|
790
|
-
window.addEventListener("message", postMessageHandler);
|
|
791
768
|
function cleanup() {
|
|
792
769
|
clearTimeout(timer);
|
|
793
770
|
clearInterval(closedPoll);
|
|
794
|
-
if (closedGraceTimer) clearTimeout(closedGraceTimer);
|
|
795
|
-
window.removeEventListener("message", postMessageHandler);
|
|
796
|
-
channel?.close();
|
|
797
771
|
}
|
|
798
772
|
});
|
|
799
773
|
}
|
|
800
774
|
var VERIFY_POPUP_TIMEOUT_MS = 6e4;
|
|
801
775
|
function findDevicePasskeyViaPopup(options) {
|
|
802
776
|
return new Promise((resolve, reject) => {
|
|
803
|
-
const
|
|
804
|
-
const payload = {
|
|
777
|
+
const verificationToken = crypto.randomUUID();
|
|
778
|
+
const payload = {
|
|
779
|
+
...options,
|
|
780
|
+
verificationToken
|
|
781
|
+
};
|
|
805
782
|
const encoded = btoa(JSON.stringify(payload));
|
|
806
783
|
const popupUrl = `${window.location.origin}/passkey-verify#${encoded}`;
|
|
807
784
|
const popup = window.open(popupUrl, "swype-passkey-verify");
|
|
@@ -810,7 +787,6 @@ function findDevicePasskeyViaPopup(options) {
|
|
|
810
787
|
return;
|
|
811
788
|
}
|
|
812
789
|
let settled = false;
|
|
813
|
-
const channel = typeof BroadcastChannel !== "undefined" ? new BroadcastChannel(channelId) : null;
|
|
814
790
|
const timer = setTimeout(() => {
|
|
815
791
|
cleanup();
|
|
816
792
|
resolve(null);
|
|
@@ -820,44 +796,28 @@ function findDevicePasskeyViaPopup(options) {
|
|
|
820
796
|
clearInterval(closedPoll);
|
|
821
797
|
setTimeout(() => {
|
|
822
798
|
if (!settled) {
|
|
799
|
+
settled = true;
|
|
823
800
|
cleanup();
|
|
824
|
-
|
|
801
|
+
checkServerForPasskeyByToken(
|
|
802
|
+
options.authToken,
|
|
803
|
+
options.apiBaseUrl,
|
|
804
|
+
verificationToken
|
|
805
|
+
).then((result) => {
|
|
806
|
+
resolve(result?.credentialId ?? null);
|
|
807
|
+
}).catch(() => {
|
|
808
|
+
resolve(null);
|
|
809
|
+
});
|
|
825
810
|
}
|
|
826
811
|
}, POPUP_CLOSED_GRACE_MS);
|
|
827
812
|
}
|
|
828
813
|
}, POPUP_CLOSED_POLL_MS);
|
|
829
|
-
function handleResult(data) {
|
|
830
|
-
if (settled) return;
|
|
831
|
-
if (!data || typeof data !== "object") return;
|
|
832
|
-
if (data.type !== "swype:passkey-verify-result") return;
|
|
833
|
-
settled = true;
|
|
834
|
-
cleanup();
|
|
835
|
-
if (data.error) {
|
|
836
|
-
resolve(null);
|
|
837
|
-
} else if (data.result && typeof data.result === "object") {
|
|
838
|
-
const result = data.result;
|
|
839
|
-
resolve(result.credentialId ?? null);
|
|
840
|
-
} else {
|
|
841
|
-
resolve(null);
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
if (channel) {
|
|
845
|
-
channel.onmessage = (event) => handleResult(event.data);
|
|
846
|
-
}
|
|
847
|
-
const postMessageHandler = (event) => {
|
|
848
|
-
if (event.source !== popup) return;
|
|
849
|
-
handleResult(event.data);
|
|
850
|
-
};
|
|
851
|
-
window.addEventListener("message", postMessageHandler);
|
|
852
814
|
function cleanup() {
|
|
853
815
|
clearTimeout(timer);
|
|
854
816
|
clearInterval(closedPoll);
|
|
855
|
-
window.removeEventListener("message", postMessageHandler);
|
|
856
|
-
channel?.close();
|
|
857
817
|
}
|
|
858
818
|
});
|
|
859
819
|
}
|
|
860
|
-
async function
|
|
820
|
+
async function checkServerForPasskeyByToken(authToken, apiBaseUrl, verificationToken) {
|
|
861
821
|
if (!authToken || !apiBaseUrl) return null;
|
|
862
822
|
const res = await fetch(`${apiBaseUrl}/v1/users/config`, {
|
|
863
823
|
headers: { Authorization: `Bearer ${authToken}` }
|
|
@@ -865,9 +825,8 @@ async function checkServerForNewPasskey(authToken, apiBaseUrl, existingCredentia
|
|
|
865
825
|
if (!res.ok) return null;
|
|
866
826
|
const body = await res.json();
|
|
867
827
|
const passkeys = body.config.passkeys ?? [];
|
|
868
|
-
const
|
|
869
|
-
|
|
870
|
-
return newPasskey ?? null;
|
|
828
|
+
const matched = passkeys.find((p) => p.lastVerificationToken === verificationToken);
|
|
829
|
+
return matched ? { credentialId: matched.credentialId, publicKey: matched.publicKey } : null;
|
|
871
830
|
}
|
|
872
831
|
|
|
873
832
|
// src/hooks.ts
|
|
@@ -5572,30 +5531,42 @@ function SwypePaymentInner({
|
|
|
5572
5531
|
authToken: token ?? void 0,
|
|
5573
5532
|
apiBaseUrl
|
|
5574
5533
|
});
|
|
5575
|
-
const { credentialId
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
);
|
|
5579
|
-
|
|
5534
|
+
const { credentialId } = await createPasskeyViaPopup(popupOptions);
|
|
5535
|
+
setActiveCredentialId(credentialId);
|
|
5536
|
+
localStorage.setItem(ACTIVE_CREDENTIAL_STORAGE_KEY, credentialId);
|
|
5537
|
+
setPasskeyPopupNeeded(false);
|
|
5538
|
+
const resolved = resolvePostAuthStep({
|
|
5539
|
+
hasPasskey: true,
|
|
5540
|
+
accounts,
|
|
5541
|
+
persistedMobileFlow: loadMobileFlowState(),
|
|
5542
|
+
mobileSetupInProgress: mobileSetupFlowRef.current,
|
|
5543
|
+
connectingNewAccount
|
|
5544
|
+
});
|
|
5545
|
+
if (resolved.clearPersistedFlow) {
|
|
5546
|
+
clearMobileFlowState();
|
|
5547
|
+
}
|
|
5548
|
+
setStep(resolved.step);
|
|
5580
5549
|
} catch (err) {
|
|
5581
5550
|
captureException(err);
|
|
5582
5551
|
setError(err instanceof Error ? err.message : "Failed to register passkey");
|
|
5583
5552
|
} finally {
|
|
5584
5553
|
setRegisteringPasskey(false);
|
|
5585
5554
|
}
|
|
5586
|
-
}, [user,
|
|
5555
|
+
}, [user, getAccessToken, apiBaseUrl, accounts, connectingNewAccount]);
|
|
5587
5556
|
const handleVerifyPasskeyViaPopup = useCallback(async () => {
|
|
5588
5557
|
setVerifyingPasskeyPopup(true);
|
|
5589
5558
|
setError(null);
|
|
5590
5559
|
try {
|
|
5560
|
+
const token = await getAccessToken();
|
|
5591
5561
|
const matched = await findDevicePasskeyViaPopup({
|
|
5592
5562
|
credentialIds: knownCredentialIds,
|
|
5593
|
-
rpId: resolvePasskeyRpId()
|
|
5563
|
+
rpId: resolvePasskeyRpId(),
|
|
5564
|
+
authToken: token ?? void 0,
|
|
5565
|
+
apiBaseUrl
|
|
5594
5566
|
});
|
|
5595
5567
|
if (matched) {
|
|
5596
5568
|
setActiveCredentialId(matched);
|
|
5597
5569
|
window.localStorage.setItem(ACTIVE_CREDENTIAL_STORAGE_KEY, matched);
|
|
5598
|
-
const token = await getAccessToken();
|
|
5599
5570
|
if (token) {
|
|
5600
5571
|
reportPasskeyActivity(apiBaseUrl, token, matched).catch(() => {
|
|
5601
5572
|
});
|