@proveanything/smartlinks-auth-ui 0.5.22 → 0.6.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/ACCOUNT_CACHING.md +349 -0
- package/ANDROID_NATIVE_BRIDGE.md +775 -0
- package/AUTH_STATE_MANAGEMENT.md +262 -0
- package/CAPACITOR_INTEGRATION.md +244 -0
- package/CUSTOMIZATION_GUIDE.md +411 -0
- package/README.md +73 -13
- package/SDK_DEBUGGING_GUIDE.md +217 -0
- package/SMARTLINKS_FRAME.md +302 -0
- package/SMARTLINKS_INTEGRATION.md +330 -0
- package/WHATSAPP_OTP_PLAN.md +106 -0
- package/dist/api.d.ts +27 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/components/ProviderButtons.d.ts +1 -0
- package/dist/components/ProviderButtons.d.ts.map +1 -1
- package/dist/components/SmartlinksAuthUI.d.ts.map +1 -1
- package/dist/context/AuthContext.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +379 -34
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +379 -33
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +98 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/persistentStorage.d.ts +14 -0
- package/dist/utils/persistentStorage.d.ts.map +1 -1
- package/dist/utils/tokenStorage.d.ts +7 -0
- package/dist/utils/tokenStorage.d.ts.map +1 -1
- package/package.json +15 -6
package/dist/index.js
CHANGED
|
@@ -336,7 +336,7 @@ const EmailAuthForm = ({ mode, onSubmit, onModeSwitch, onForgotPassword, loading
|
|
|
336
336
|
}, disabled: loading })), jsxRuntime.jsxs("div", { className: "auth-form-header", children: [jsxRuntime.jsx("h2", { className: "auth-form-title", children: title }), jsxRuntime.jsx("p", { className: "auth-form-subtitle", children: subtitle })] }), error && (jsxRuntime.jsxs("div", { className: "auth-error", role: "alert", children: [jsxRuntime.jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "currentColor", children: jsxRuntime.jsx("path", { d: "M8 0C3.58 0 0 3.58 0 8s3.58 8 8 8 8-3.58 8-8-3.58-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z" }) }), error] })), mode === 'register' && (jsxRuntime.jsxs("div", { className: "auth-form-group", children: [jsxRuntime.jsx("label", { htmlFor: "displayName", className: "auth-label", children: "Full Name" }), jsxRuntime.jsx("input", { type: "text", id: "displayName", className: "auth-input", value: formData.displayName || '', onChange: (e) => handleChange('displayName', e.target.value), required: mode === 'register', disabled: loading, placeholder: "John Smith" })] })), jsxRuntime.jsxs("div", { className: "auth-form-group", children: [jsxRuntime.jsx("label", { htmlFor: "email", className: "auth-label", children: "Email address" }), jsxRuntime.jsx("input", { type: "email", id: "email", className: "auth-input", value: formData.email || '', onChange: (e) => handleChange('email', e.target.value), required: true, disabled: loading, placeholder: "you@example.com", autoComplete: "email" })] }), jsxRuntime.jsxs("div", { className: "auth-form-group", children: [jsxRuntime.jsx("label", { htmlFor: "password", className: "auth-label", children: "Password" }), jsxRuntime.jsx("input", { type: "password", id: "password", className: "auth-input", value: formData.password || '', onChange: (e) => handleChange('password', e.target.value), required: true, disabled: loading, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022", autoComplete: mode === 'login' ? 'current-password' : 'new-password', minLength: 6 })] }), mode === 'register' && hasSchemaFields && schemaFields.inline.map(renderSchemaField), mode === 'register' && hasLegacyFields && additionalFields.map(renderLegacyField), mode === 'register' && hasSchemaFields && schemaFields.postCredentials.length > 0 && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: "auth-divider", style: { margin: '16px 0' }, children: jsxRuntime.jsx("span", { children: "Additional Information" }) }), schemaFields.postCredentials.map(renderSchemaField)] })), mode === 'login' && (jsxRuntime.jsx("div", { className: "auth-form-footer", children: jsxRuntime.jsx("button", { type: "button", className: "auth-link", onClick: onForgotPassword, disabled: loading, children: "Forgot password?" }) })), jsxRuntime.jsx("button", { type: "submit", className: "auth-button auth-button-primary", disabled: loading, children: loading ? (jsxRuntime.jsx("span", { className: "auth-spinner" })) : mode === 'login' ? ('Sign in') : ('Create account') }), signupProminence !== 'balanced' && signupProminence !== 'none' && (jsxRuntime.jsxs("div", { className: "auth-divider", children: [jsxRuntime.jsx("span", { children: mode === 'login' ? "Don't have an account?" : 'Already have an account?' }), signupRedirectUrl && mode === 'login' ? (jsxRuntime.jsx("a", { href: signupRedirectUrl, target: "_top", className: "auth-link auth-link-bold", children: "Sign up" })) : (jsxRuntime.jsx("button", { type: "button", className: "auth-link auth-link-bold", onClick: onModeSwitch, disabled: loading, children: mode === 'login' ? 'Sign up' : 'Sign in' }))] }))] }));
|
|
337
337
|
};
|
|
338
338
|
|
|
339
|
-
const ProviderButtons = ({ enabledProviders, providerOrder, onEmailLogin, onGoogleLogin, onPhoneLogin, onMagicLinkLogin, onWhatsAppLogin, loading, }) => {
|
|
339
|
+
const ProviderButtons = ({ enabledProviders, providerOrder, onEmailLogin, onGoogleLogin, onAppleLogin, onPhoneLogin, onMagicLinkLogin, onWhatsAppLogin, loading, }) => {
|
|
340
340
|
if (enabledProviders.length === 0)
|
|
341
341
|
return null;
|
|
342
342
|
// Determine the order of providers to display
|
|
@@ -359,6 +359,11 @@ const ProviderButtons = ({ enabledProviders, providerOrder, onEmailLogin, onGoog
|
|
|
359
359
|
icon: (jsxRuntime.jsxs("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", children: [jsxRuntime.jsx("path", { d: "M19.6 10.227c0-.709-.064-1.39-.182-2.045H10v3.868h5.382a4.6 4.6 0 01-1.996 3.018v2.51h3.232c1.891-1.742 2.982-4.305 2.982-7.35z", fill: "#4285F4" }), jsxRuntime.jsx("path", { d: "M10 20c2.7 0 4.964-.895 6.618-2.423l-3.232-2.509c-.895.6-2.04.955-3.386.955-2.605 0-4.81-1.76-5.595-4.123H1.064v2.59A9.996 9.996 0 0010 20z", fill: "#34A853" }), jsxRuntime.jsx("path", { d: "M4.405 11.9c-.2-.6-.314-1.24-.314-1.9 0-.66.114-1.3.314-1.9V5.51H1.064A9.996 9.996 0 000 10c0 1.614.386 3.14 1.064 4.49l3.34-2.59z", fill: "#FBBC05" }), jsxRuntime.jsx("path", { d: "M10 3.977c1.468 0 2.786.505 3.823 1.496l2.868-2.868C14.959.99 12.695 0 10 0 6.09 0 2.71 2.24 1.064 5.51l3.34 2.59C5.19 5.736 7.395 3.977 10 3.977z", fill: "#EA4335" })] })),
|
|
360
360
|
onClick: onGoogleLogin
|
|
361
361
|
},
|
|
362
|
+
apple: {
|
|
363
|
+
label: 'Continue with Apple',
|
|
364
|
+
icon: (jsxRuntime.jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true", children: jsxRuntime.jsx("path", { d: "M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09l.01-.01zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z" }) })),
|
|
365
|
+
onClick: () => onAppleLogin?.()
|
|
366
|
+
},
|
|
362
367
|
phone: {
|
|
363
368
|
label: 'Continue with Phone',
|
|
364
369
|
icon: (jsxRuntime.jsx("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", children: jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V15a2 2 0 01-2 2h-1C7.82 17 2 11.18 2 5V4z" }) })),
|
|
@@ -11430,6 +11435,25 @@ class AuthAPI {
|
|
|
11430
11435
|
redirectUri,
|
|
11431
11436
|
});
|
|
11432
11437
|
}
|
|
11438
|
+
/**
|
|
11439
|
+
* Sign in with Apple via an identity token (from native "Sign in with Apple"
|
|
11440
|
+
* or the web JS flow). The backend verifies the JWT against Apple's keys.
|
|
11441
|
+
*
|
|
11442
|
+
* Delegates to `smartlinks.authKit.appleLogin`, which on success stores the
|
|
11443
|
+
* bearer token automatically and invalidates the SDK cache.
|
|
11444
|
+
*/
|
|
11445
|
+
async loginWithApple(identityToken, options) {
|
|
11446
|
+
this.log.log('loginWithApple called:', {
|
|
11447
|
+
tokenLength: identityToken?.length,
|
|
11448
|
+
hasAuthCode: !!options?.authorizationCode,
|
|
11449
|
+
hasUserInfo: !!options?.appleUserInfo,
|
|
11450
|
+
});
|
|
11451
|
+
return smartlinks__namespace.authKit.appleLogin(this.clientId, identityToken, {
|
|
11452
|
+
authorizationCode: options?.authorizationCode,
|
|
11453
|
+
nonce: options?.nonce,
|
|
11454
|
+
userInfo: options?.appleUserInfo,
|
|
11455
|
+
});
|
|
11456
|
+
}
|
|
11433
11457
|
async sendPhoneCode(phoneNumber) {
|
|
11434
11458
|
return smartlinks__namespace.authKit.sendPhoneCode(this.clientId, phoneNumber);
|
|
11435
11459
|
}
|
|
@@ -11478,6 +11502,17 @@ class AuthAPI {
|
|
|
11478
11502
|
throw error;
|
|
11479
11503
|
}
|
|
11480
11504
|
}
|
|
11505
|
+
/**
|
|
11506
|
+
* Complete a password reset OR accept an invite by setting the first password.
|
|
11507
|
+
*
|
|
11508
|
+
* The backend distinguishes the two flows via the token's `metadata.invitedBy`:
|
|
11509
|
+
* - Plain password reset → returns `{ success, message }` only.
|
|
11510
|
+
* - Invite acceptance under `verify-auto-login` → returns a full session
|
|
11511
|
+
* (`token`, `user`, `accountData`, and on native: refresh-token fields)
|
|
11512
|
+
* so the kit can log the user straight in without bouncing them to /login.
|
|
11513
|
+
*
|
|
11514
|
+
* See: SDK_AUTHKIT_REFRESH_TOKENS / "Invite Auto-Login" spec.
|
|
11515
|
+
*/
|
|
11481
11516
|
async completePasswordReset(token, newPassword) {
|
|
11482
11517
|
return smartlinks__namespace.authKit.completePasswordReset(this.clientId, token, newPassword);
|
|
11483
11518
|
}
|
|
@@ -11960,6 +11995,24 @@ async function getStorage() {
|
|
|
11960
11995
|
storageInstance = await storageInitPromise;
|
|
11961
11996
|
return storageInstance;
|
|
11962
11997
|
}
|
|
11998
|
+
/**
|
|
11999
|
+
* Install a custom storage backend (e.g. Capacitor Keychain / Keystore).
|
|
12000
|
+
*
|
|
12001
|
+
* Must be called BEFORE `getStorage()` is invoked for the first time
|
|
12002
|
+
* (typically right after your app boots, before `<SmartlinksAuthUI />` mounts).
|
|
12003
|
+
* The supplied adapter then handles ALL token, user, and account persistence
|
|
12004
|
+
* — replacing the default IndexedDB → localStorage → in-memory chain.
|
|
12005
|
+
*
|
|
12006
|
+
* Intended for native shells (Capacitor / Capgo) that want tokens stored in
|
|
12007
|
+
* the OS secure enclave (Keychain on iOS, Keystore on Android) instead of the
|
|
12008
|
+
* WebView's IndexedDB. Implement the `PersistentStorage` interface against
|
|
12009
|
+
* your secure-storage plugin and pass the instance here.
|
|
12010
|
+
*/
|
|
12011
|
+
function setStorageAdapter(adapter) {
|
|
12012
|
+
StorageFactory.reset();
|
|
12013
|
+
storageInstance = adapter;
|
|
12014
|
+
storageInitPromise = Promise.resolve(adapter);
|
|
12015
|
+
}
|
|
11963
12016
|
/**
|
|
11964
12017
|
* Listen for storage changes from other tabs
|
|
11965
12018
|
*/
|
|
@@ -11978,6 +12031,7 @@ function onStorageChange(callback) {
|
|
|
11978
12031
|
}
|
|
11979
12032
|
|
|
11980
12033
|
const TOKEN_KEY = 'token';
|
|
12034
|
+
const REFRESH_TOKEN_KEY = 'refresh_token';
|
|
11981
12035
|
const USER_KEY = 'user';
|
|
11982
12036
|
const ACCOUNT_DATA_KEY = 'account_data';
|
|
11983
12037
|
const ACCOUNT_INFO_KEY = 'account_info';
|
|
@@ -12016,6 +12070,28 @@ const tokenStorage = {
|
|
|
12016
12070
|
const storage = await getStorage();
|
|
12017
12071
|
await storage.removeItem(TOKEN_KEY);
|
|
12018
12072
|
},
|
|
12073
|
+
// ----- Refresh token (native / long-lived sessions) ------------------
|
|
12074
|
+
async saveRefreshToken(token, expiresAt) {
|
|
12075
|
+
const storage = await getStorage();
|
|
12076
|
+
const payload = { token, expiresAt };
|
|
12077
|
+
await storage.setItem(REFRESH_TOKEN_KEY, payload);
|
|
12078
|
+
},
|
|
12079
|
+
async getRefreshToken() {
|
|
12080
|
+
const storage = await getStorage();
|
|
12081
|
+
const rt = await storage.getItem(REFRESH_TOKEN_KEY);
|
|
12082
|
+
if (!rt)
|
|
12083
|
+
return null;
|
|
12084
|
+
if (rt.expiresAt && rt.expiresAt < Date.now()) {
|
|
12085
|
+
console.log('[TokenStorage] Refresh token expired - clearing');
|
|
12086
|
+
await this.clearRefreshToken();
|
|
12087
|
+
return null;
|
|
12088
|
+
}
|
|
12089
|
+
return rt;
|
|
12090
|
+
},
|
|
12091
|
+
async clearRefreshToken() {
|
|
12092
|
+
const storage = await getStorage();
|
|
12093
|
+
await storage.removeItem(REFRESH_TOKEN_KEY);
|
|
12094
|
+
},
|
|
12019
12095
|
async saveUser(user) {
|
|
12020
12096
|
const storage = await getStorage();
|
|
12021
12097
|
await storage.setItem(USER_KEY, user);
|
|
@@ -12030,6 +12106,7 @@ const tokenStorage = {
|
|
|
12030
12106
|
},
|
|
12031
12107
|
async clearAll() {
|
|
12032
12108
|
await this.clearToken();
|
|
12109
|
+
await this.clearRefreshToken();
|
|
12033
12110
|
await this.clearUser();
|
|
12034
12111
|
await this.clearAccountData();
|
|
12035
12112
|
await this.clearAccountInfo();
|
|
@@ -12087,12 +12164,18 @@ const tokenStorage = {
|
|
|
12087
12164
|
|
|
12088
12165
|
// Export context for optional usage (e.g., SmartlinksFrame can work without AuthProvider)
|
|
12089
12166
|
const AuthContext = React.createContext(undefined);
|
|
12090
|
-
const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 * 1000, preloadAccountInfo = false,
|
|
12167
|
+
const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 * 1000, preloadAccountInfo = false, clientId: clientIdProp,
|
|
12091
12168
|
// Token refresh settings
|
|
12092
12169
|
enableAutoRefresh = true, refreshThresholdPercent = 75, // Refresh when 75% of token lifetime has passed
|
|
12093
12170
|
refreshCheckInterval = 60 * 1000, // Check every minute
|
|
12171
|
+
refreshOnResume,
|
|
12094
12172
|
// Contact & Interaction features
|
|
12095
12173
|
collectionId, enableContactSync, enableInteractionTracking, interactionAppId, interactionConfig, }) => {
|
|
12174
|
+
// Resolved client ID — explicit prop wins; otherwise fall back to the
|
|
12175
|
+
// collection's defaultAuthKitId (the standard pattern — see
|
|
12176
|
+
// mem://architecture/default-authkit-collection-property).
|
|
12177
|
+
const [resolvedClientId, setResolvedClientId] = React.useState(clientIdProp);
|
|
12178
|
+
const clientId = resolvedClientId;
|
|
12096
12179
|
const [user, setUser] = React.useState(null);
|
|
12097
12180
|
const [token, setToken] = React.useState(null);
|
|
12098
12181
|
const [accountData, setAccountData] = React.useState(null);
|
|
@@ -12108,8 +12191,8 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
12108
12191
|
const pendingVerificationRef = React.useRef(false);
|
|
12109
12192
|
const proxyRefreshInFlightRef = React.useRef(false);
|
|
12110
12193
|
// Stable refs for callbacks to avoid useEffect dependency cycles
|
|
12111
|
-
const syncContactRef = React.useRef();
|
|
12112
|
-
const trackInteractionRef = React.useRef();
|
|
12194
|
+
const syncContactRef = React.useRef(undefined);
|
|
12195
|
+
const trackInteractionRef = React.useRef(undefined);
|
|
12113
12196
|
// Default to enabled if collectionId is provided
|
|
12114
12197
|
const shouldSyncContacts = enableContactSync ?? !!collectionId;
|
|
12115
12198
|
const shouldTrackInteractions = enableInteractionTracking ?? !!collectionId;
|
|
@@ -12792,9 +12875,7 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
12792
12875
|
});
|
|
12793
12876
|
});
|
|
12794
12877
|
};
|
|
12795
|
-
const login = React.useCallback(async (authToken, authUser, authAccountData, isNewUser, expiresAt
|
|
12796
|
-
// Note: waitForParentAck removed - we ALWAYS wait for parent ack in iframe mode now
|
|
12797
|
-
) => {
|
|
12878
|
+
const login = React.useCallback(async (authToken, authUser, authAccountData, isNewUser, expiresAt, refreshTokenValue, refreshTokenExpiresAt) => {
|
|
12798
12879
|
try {
|
|
12799
12880
|
// Only persist to storage in standalone mode
|
|
12800
12881
|
if (!proxyMode) {
|
|
@@ -12808,6 +12889,15 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
12808
12889
|
else {
|
|
12809
12890
|
await tokenStorage.clearAccountData();
|
|
12810
12891
|
}
|
|
12892
|
+
// Persist refresh token when the backend issued one (native clients).
|
|
12893
|
+
if (refreshTokenValue) {
|
|
12894
|
+
const rtExp = refreshTokenExpiresAt
|
|
12895
|
+
?? Date.now() + 90 * 24 * 60 * 60 * 1000; // 90d fallback per spec
|
|
12896
|
+
await tokenStorage.saveRefreshToken(refreshTokenValue, rtExp);
|
|
12897
|
+
}
|
|
12898
|
+
else {
|
|
12899
|
+
await tokenStorage.clearRefreshToken();
|
|
12900
|
+
}
|
|
12811
12901
|
smartlinks__namespace.auth.verifyToken(authToken).catch(() => { });
|
|
12812
12902
|
}
|
|
12813
12903
|
// Always update memory state (but NOT isVerified yet - wait for parent ack in iframe mode)
|
|
@@ -12871,6 +12961,17 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
12871
12961
|
}
|
|
12872
12962
|
// Only clear persistent storage in standalone mode
|
|
12873
12963
|
if (!proxyMode) {
|
|
12964
|
+
// Best-effort: revoke the refresh-token family server-side before
|
|
12965
|
+
// wiping local state, so a stolen copy is dead immediately.
|
|
12966
|
+
try {
|
|
12967
|
+
const storedRefresh = await tokenStorage.getRefreshToken();
|
|
12968
|
+
if (storedRefresh?.token && clientId) {
|
|
12969
|
+
await smartlinks__namespace.authKit.logout(clientId, storedRefresh.token);
|
|
12970
|
+
}
|
|
12971
|
+
}
|
|
12972
|
+
catch (err) {
|
|
12973
|
+
console.warn('[AuthContext] Refresh-token revoke failed (continuing):', err);
|
|
12974
|
+
}
|
|
12874
12975
|
await tokenStorage.clearAll();
|
|
12875
12976
|
smartlinks__namespace.auth.logout();
|
|
12876
12977
|
}
|
|
@@ -12896,7 +12997,7 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
12896
12997
|
catch (error) {
|
|
12897
12998
|
console.error('Failed to clear auth data from storage:', error);
|
|
12898
12999
|
}
|
|
12899
|
-
}, [proxyMode, notifyAuthStateChange, user, contactId, collectionId, shouldTrackInteractions, trackInteraction]);
|
|
13000
|
+
}, [proxyMode, clientId, notifyAuthStateChange, user, contactId, collectionId, shouldTrackInteractions, trackInteraction]);
|
|
12900
13001
|
const getToken = React.useCallback(async () => {
|
|
12901
13002
|
if (proxyMode) {
|
|
12902
13003
|
return token;
|
|
@@ -12923,12 +13024,44 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
12923
13024
|
expiresIn: Math.max(0, storedToken.expiresAt - Date.now()),
|
|
12924
13025
|
};
|
|
12925
13026
|
}, [proxyMode, token]);
|
|
12926
|
-
// Refresh token -
|
|
13027
|
+
// Refresh token - prefers the AuthKit refresh-token endpoint when a refresh
|
|
13028
|
+
// token is stored (native clients); otherwise falls back to verify-and-extend
|
|
13029
|
+
// for web clients whose backend still issues only short-lived JWTs.
|
|
12927
13030
|
const refreshToken = React.useCallback(async () => {
|
|
12928
13031
|
if (proxyMode) {
|
|
12929
13032
|
throw new Error('Token refresh in proxy mode is handled by the parent application');
|
|
12930
13033
|
}
|
|
12931
13034
|
const storedToken = await tokenStorage.getToken();
|
|
13035
|
+
const storedRefresh = await tokenStorage.getRefreshToken();
|
|
13036
|
+
// -------- Path A: true refresh-token rotation (native) ---------------
|
|
13037
|
+
if (storedRefresh?.token && clientId) {
|
|
13038
|
+
try {
|
|
13039
|
+
const resp = await smartlinks__namespace.authKit.refreshToken(clientId, storedRefresh.token);
|
|
13040
|
+
await tokenStorage.saveToken(resp.token, resp.expiresAt);
|
|
13041
|
+
await tokenStorage.saveRefreshToken(resp.refreshToken, resp.refreshTokenExpiresAt);
|
|
13042
|
+
setToken(resp.token);
|
|
13043
|
+
setIsVerified(true);
|
|
13044
|
+
pendingVerificationRef.current = false;
|
|
13045
|
+
notifyAuthStateChange('TOKEN_REFRESH', user, resp.token, accountData, accountInfo, true, contact, contactId);
|
|
13046
|
+
return resp.token;
|
|
13047
|
+
}
|
|
13048
|
+
catch (error) {
|
|
13049
|
+
console.error('[AuthContext] Refresh-token rotation failed:', error);
|
|
13050
|
+
if (isNetworkError(error))
|
|
13051
|
+
throw error;
|
|
13052
|
+
// Reuse / invalid / expired → force re-login. The SDK surfaces these
|
|
13053
|
+
// as RefreshErrorCode (INVALID_REFRESH_TOKEN / REUSE_DETECTED / MISSING).
|
|
13054
|
+
const code = error?.errorCode ?? error?.code;
|
|
13055
|
+
if (code === 'INVALID_REFRESH_TOKEN' ||
|
|
13056
|
+
code === 'REFRESH_TOKEN_REUSE_DETECTED' ||
|
|
13057
|
+
code === 'MISSING_REFRESH_TOKEN') {
|
|
13058
|
+
await logout();
|
|
13059
|
+
throw new Error('Session expired. Please login again.');
|
|
13060
|
+
}
|
|
13061
|
+
// Fall through to verify-and-extend on unexpected errors.
|
|
13062
|
+
}
|
|
13063
|
+
}
|
|
13064
|
+
// -------- Path B: legacy verify-and-extend (web) ---------------------
|
|
12932
13065
|
if (!storedToken?.token) {
|
|
12933
13066
|
throw new Error('No token to refresh. Please login first.');
|
|
12934
13067
|
}
|
|
@@ -12953,7 +13086,7 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
12953
13086
|
await logout();
|
|
12954
13087
|
throw new Error('Token refresh failed. Please login again.');
|
|
12955
13088
|
}
|
|
12956
|
-
}, [proxyMode, user, accountData, accountInfo, contact, contactId, notifyAuthStateChange, isNetworkError, logout]);
|
|
13089
|
+
}, [proxyMode, clientId, user, accountData, accountInfo, contact, contactId, notifyAuthStateChange, isNetworkError, logout]);
|
|
12957
13090
|
const getAccount = React.useCallback(async (forceRefresh = false) => {
|
|
12958
13091
|
try {
|
|
12959
13092
|
if (proxyMode) {
|
|
@@ -13117,6 +13250,105 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
13117
13250
|
clearInterval(intervalId);
|
|
13118
13251
|
};
|
|
13119
13252
|
}, [proxyMode, enableAutoRefresh, refreshCheckInterval, refreshThresholdPercent, token, user, refreshToken]);
|
|
13253
|
+
// Resolve clientId from the collection's defaultAuthKitId when not provided
|
|
13254
|
+
// explicitly. Most apps only pass `collectionId` — this means refresh-token
|
|
13255
|
+
// rotation and server-side logout revocation still work end-to-end.
|
|
13256
|
+
React.useEffect(() => {
|
|
13257
|
+
if (clientIdProp) {
|
|
13258
|
+
setResolvedClientId(clientIdProp);
|
|
13259
|
+
return;
|
|
13260
|
+
}
|
|
13261
|
+
if (!collectionId)
|
|
13262
|
+
return;
|
|
13263
|
+
let cancelled = false;
|
|
13264
|
+
(async () => {
|
|
13265
|
+
try {
|
|
13266
|
+
const { getDefaultAuthKitId } = await Promise.resolve().then(function () { return defaultAuthKit; });
|
|
13267
|
+
const id = await getDefaultAuthKitId(collectionId);
|
|
13268
|
+
if (!cancelled && id)
|
|
13269
|
+
setResolvedClientId(id);
|
|
13270
|
+
}
|
|
13271
|
+
catch (err) {
|
|
13272
|
+
console.warn('[AuthContext] Could not resolve default authKitId from collection:', err);
|
|
13273
|
+
}
|
|
13274
|
+
})();
|
|
13275
|
+
return () => { cancelled = true; };
|
|
13276
|
+
}, [clientIdProp, collectionId]);
|
|
13277
|
+
// Refresh-on-resume: fires when the app returns to the foreground.
|
|
13278
|
+
// - Capacitor: `App.addListener('appStateChange')` via dynamic import (no
|
|
13279
|
+
// runtime dep on web).
|
|
13280
|
+
// - Universal fallback: `document.visibilitychange` — also catches PWAs /
|
|
13281
|
+
// web tabs that have been backgrounded for hours.
|
|
13282
|
+
React.useEffect(() => {
|
|
13283
|
+
if (proxyMode || !enableAutoRefresh || !token || !user)
|
|
13284
|
+
return;
|
|
13285
|
+
// Default ON when we detect a native shell; OFF otherwise.
|
|
13286
|
+
const isNative = (() => {
|
|
13287
|
+
try {
|
|
13288
|
+
const cap = window.Capacitor;
|
|
13289
|
+
if (cap?.isNativePlatform?.() === true)
|
|
13290
|
+
return true;
|
|
13291
|
+
if (window.AuthKit?.isNative === true)
|
|
13292
|
+
return true;
|
|
13293
|
+
}
|
|
13294
|
+
catch { /* noop */ }
|
|
13295
|
+
return false;
|
|
13296
|
+
})();
|
|
13297
|
+
const enabled = refreshOnResume ?? isNative;
|
|
13298
|
+
if (!enabled)
|
|
13299
|
+
return;
|
|
13300
|
+
const maybeRefresh = async () => {
|
|
13301
|
+
try {
|
|
13302
|
+
const storedToken = await tokenStorage.getToken();
|
|
13303
|
+
if (!storedToken?.expiresAt)
|
|
13304
|
+
return;
|
|
13305
|
+
const remainingMs = storedToken.expiresAt - Date.now();
|
|
13306
|
+
const totalLifetimeMs = 7 * 24 * 60 * 60 * 1000;
|
|
13307
|
+
const percentUsed = ((totalLifetimeMs - remainingMs) / totalLifetimeMs) * 100;
|
|
13308
|
+
if (percentUsed < refreshThresholdPercent)
|
|
13309
|
+
return;
|
|
13310
|
+
await refreshToken().catch(() => { });
|
|
13311
|
+
}
|
|
13312
|
+
catch (err) {
|
|
13313
|
+
console.error('[AuthContext] Resume refresh check failed:', err);
|
|
13314
|
+
}
|
|
13315
|
+
};
|
|
13316
|
+
// Universal visibility listener
|
|
13317
|
+
const onVisibility = () => {
|
|
13318
|
+
if (document.visibilityState === 'visible')
|
|
13319
|
+
maybeRefresh();
|
|
13320
|
+
};
|
|
13321
|
+
document.addEventListener('visibilitychange', onVisibility);
|
|
13322
|
+
// Capacitor listener (optional)
|
|
13323
|
+
let capRemove;
|
|
13324
|
+
(async () => {
|
|
13325
|
+
try {
|
|
13326
|
+
// Only attempt to load @capacitor/app in an actual native shell.
|
|
13327
|
+
// The specifier is obfuscated so bundlers (Vite/webpack) don't try
|
|
13328
|
+
// to statically resolve an optional peer dependency at build time.
|
|
13329
|
+
const isNative = typeof window !== 'undefined' &&
|
|
13330
|
+
(window.Capacitor?.isNativePlatform?.() === true ||
|
|
13331
|
+
window.AuthKit?.isNative === true);
|
|
13332
|
+
if (!isNative)
|
|
13333
|
+
return;
|
|
13334
|
+
const specifier = ['@capacitor', 'app'].join('/');
|
|
13335
|
+
const dynamicImport = new Function('s', 'return import(s)');
|
|
13336
|
+
const mod = await dynamicImport(specifier);
|
|
13337
|
+
const handle = await mod.App.addListener('appStateChange', (state) => {
|
|
13338
|
+
if (state.isActive)
|
|
13339
|
+
maybeRefresh();
|
|
13340
|
+
});
|
|
13341
|
+
capRemove = () => handle?.remove?.();
|
|
13342
|
+
}
|
|
13343
|
+
catch {
|
|
13344
|
+
// @capacitor/app not installed or unavailable — visibilitychange handles it.
|
|
13345
|
+
}
|
|
13346
|
+
})();
|
|
13347
|
+
return () => {
|
|
13348
|
+
document.removeEventListener('visibilitychange', onVisibility);
|
|
13349
|
+
capRemove?.();
|
|
13350
|
+
};
|
|
13351
|
+
}, [proxyMode, enableAutoRefresh, refreshOnResume, refreshThresholdPercent, token, user, refreshToken]);
|
|
13120
13352
|
const value = {
|
|
13121
13353
|
user,
|
|
13122
13354
|
token,
|
|
@@ -13378,8 +13610,22 @@ const loadGoogleIdentityServices = () => {
|
|
|
13378
13610
|
document.head.appendChild(script);
|
|
13379
13611
|
});
|
|
13380
13612
|
};
|
|
13613
|
+
// Helper to detect a Capacitor native runtime (iOS/Android shell, not the web)
|
|
13614
|
+
const isCapacitorNative = () => {
|
|
13615
|
+
const cap = window.Capacitor;
|
|
13616
|
+
if (!cap)
|
|
13617
|
+
return false;
|
|
13618
|
+
// isNativePlatform() is the modern API; fall back to platform string for older cores
|
|
13619
|
+
if (typeof cap.isNativePlatform === 'function')
|
|
13620
|
+
return cap.isNativePlatform();
|
|
13621
|
+
return cap.platform === 'ios' || cap.platform === 'android';
|
|
13622
|
+
};
|
|
13381
13623
|
// Helper to detect WebView environments (Android/iOS)
|
|
13382
13624
|
const detectWebView = () => {
|
|
13625
|
+
// Capacitor apps run inside a WebView (WKWebView/Android WebView) but don't
|
|
13626
|
+
// always set the `wv` UA token — treat them as WebView explicitly.
|
|
13627
|
+
if (isCapacitorNative())
|
|
13628
|
+
return true;
|
|
13383
13629
|
const ua = navigator.userAgent;
|
|
13384
13630
|
// Android WebView detection
|
|
13385
13631
|
if (/Android/i.test(ua)) {
|
|
@@ -13487,7 +13733,7 @@ const checkSilentGoogleSignIn = async (clientId, googleClientId) => {
|
|
|
13487
13733
|
});
|
|
13488
13734
|
};
|
|
13489
13735
|
// getFriendlyErrorMessage is now imported from ../utils/errorHandling
|
|
13490
|
-
const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAuthSuccess, onAuthError, onRedirect, enabledProviders = ['email', 'google', 'phone'], initialMode, signupProminence, redirectUrl, theme = 'light', className, customization, skipConfigFetch = false, minimal = false, logger, proxyMode = false, collectionId, disableConfigCache = false, enableSilentGoogleSignIn = false, whatsappReply, whatsappPrefillMessage, }) => {
|
|
13736
|
+
const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAuthSuccess, onAuthError, onRedirect, enabledProviders = ['email', 'google', 'phone'], initialMode, signupProminence, redirectUrl, theme = 'light', className, customization, skipConfigFetch = false, minimal = false, logger, proxyMode = false, collectionId, disableConfigCache = false, enableSilentGoogleSignIn = false, nativeAuth, enableSilentNativeSignIn = false, whatsappReply, whatsappPrefillMessage, }) => {
|
|
13491
13737
|
// Resolve signup prominence from props, customization, config, or default
|
|
13492
13738
|
const resolvedSignupProminence = signupProminence || customization?.signupProminence || 'minimal';
|
|
13493
13739
|
// Determine initial mode based on signupProminence setting
|
|
@@ -13787,23 +14033,28 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13787
14033
|
};
|
|
13788
14034
|
fetchSchema();
|
|
13789
14035
|
}, [collectionId, sdkReady]);
|
|
13790
|
-
// Silent
|
|
13791
|
-
//
|
|
14036
|
+
// Silent Sign-In check on mount (for native apps).
|
|
14037
|
+
// Prefers the injected `nativeAuth.checkSignIn` adapter (Capacitor) when
|
|
14038
|
+
// `enableSilentNativeSignIn` is set; otherwise falls back to the legacy
|
|
14039
|
+
// `window.AuthKit` bridge when `enableSilentGoogleSignIn` is set.
|
|
14040
|
+
const wantsSilentNative = enableSilentNativeSignIn && !!nativeAuth?.checkSignIn;
|
|
13792
14041
|
React.useEffect(() => {
|
|
13793
|
-
if (!enableSilentGoogleSignIn || silentSignInChecked || !sdkReady || auth.isAuthenticated) {
|
|
14042
|
+
if ((!enableSilentGoogleSignIn && !wantsSilentNative) || silentSignInChecked || !sdkReady || auth.isAuthenticated) {
|
|
13794
14043
|
return;
|
|
13795
14044
|
}
|
|
13796
14045
|
const googleClientId = config?.googleClientId || DEFAULT_GOOGLE_CLIENT_ID;
|
|
13797
14046
|
const performSilentSignIn = async () => {
|
|
13798
14047
|
try {
|
|
13799
|
-
const result =
|
|
14048
|
+
const result = wantsSilentNative
|
|
14049
|
+
? await nativeAuth.checkSignIn({ serverClientId: googleClientId })
|
|
14050
|
+
: await checkSilentGoogleSignIn(clientId, googleClientId);
|
|
13800
14051
|
setSilentSignInChecked(true);
|
|
13801
14052
|
if (result?.isSignedIn && result.idToken) {
|
|
13802
14053
|
setLoading(true);
|
|
13803
14054
|
try {
|
|
13804
14055
|
const authResponse = await api.loginWithGoogle(result.idToken);
|
|
13805
14056
|
if (authResponse.token) {
|
|
13806
|
-
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, false, getExpirationFromResponse(authResponse));
|
|
14057
|
+
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, false, getExpirationFromResponse(authResponse), authResponse.refreshToken, authResponse.refreshTokenExpiresAt);
|
|
13807
14058
|
setAuthSuccess(true);
|
|
13808
14059
|
setSuccessMessage('Signed in automatically with Google!');
|
|
13809
14060
|
onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
|
|
@@ -13822,7 +14073,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13822
14073
|
}
|
|
13823
14074
|
};
|
|
13824
14075
|
performSilentSignIn();
|
|
13825
|
-
}, [enableSilentGoogleSignIn, silentSignInChecked, sdkReady, auth.isAuthenticated, clientId, config?.googleClientId, api, auth, onAuthSuccess]);
|
|
14076
|
+
}, [enableSilentGoogleSignIn, wantsSilentNative, nativeAuth, silentSignInChecked, sdkReady, auth.isAuthenticated, clientId, config?.googleClientId, api, auth, onAuthSuccess]);
|
|
13826
14077
|
// Reset showEmailForm when mode changes away from login/register
|
|
13827
14078
|
React.useEffect(() => {
|
|
13828
14079
|
if (mode !== 'login' && mode !== 'register') {
|
|
@@ -13898,7 +14149,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13898
14149
|
if ((verificationMode === 'verify-auto-login' || verificationMode === 'immediate') && response.token) {
|
|
13899
14150
|
// Auto-login modes: Log the user in immediately if token is provided
|
|
13900
14151
|
// Always await - auth.login now waits for parent ack automatically in iframe mode
|
|
13901
|
-
await auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response));
|
|
14152
|
+
await auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response), response.refreshToken, response.refreshTokenExpiresAt);
|
|
13902
14153
|
setUrlAuthProcessing(false); // Clear processing state before redirect/success
|
|
13903
14154
|
if (redirectUrl) {
|
|
13904
14155
|
// Redirect to clean URL and resume flow
|
|
@@ -13958,7 +14209,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13958
14209
|
// Auto-login with magic link if token is provided
|
|
13959
14210
|
if (response.token) {
|
|
13960
14211
|
// Always await - auth.login now waits for parent ack automatically in iframe mode
|
|
13961
|
-
await auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response));
|
|
14212
|
+
await auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response), response.refreshToken, response.refreshTokenExpiresAt);
|
|
13962
14213
|
setUrlAuthProcessing(false); // Clear processing state before redirect/success
|
|
13963
14214
|
if (redirectUrl) {
|
|
13964
14215
|
// Redirect to clean URL and resume flow
|
|
@@ -14033,7 +14284,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14033
14284
|
log.log('Google OAuth code exchange response:', { hasToken: !!response.token, hasUser: !!response.user, isNewUser: response.isNewUser });
|
|
14034
14285
|
if (response.token) {
|
|
14035
14286
|
// Await login to ensure token is persisted before any navigation
|
|
14036
|
-
await auth.login(response.token, response.user, response.accountData, response.isNewUser, getExpirationFromResponse(response));
|
|
14287
|
+
await auth.login(response.token, response.user, response.accountData, response.isNewUser, getExpirationFromResponse(response), response.refreshToken, response.refreshTokenExpiresAt);
|
|
14037
14288
|
setAuthSuccess(true);
|
|
14038
14289
|
setSuccessMessage('Google login successful!');
|
|
14039
14290
|
onAuthSuccess(response.token, response.user, response.accountData);
|
|
@@ -14087,7 +14338,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14087
14338
|
// Handle different verification modes
|
|
14088
14339
|
if (verificationMode === 'immediate' && response.token) {
|
|
14089
14340
|
// Immediate mode: Log in right away if token is provided (isNewUser=true for registration)
|
|
14090
|
-
await auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response));
|
|
14341
|
+
await auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response), response.refreshToken, response.refreshTokenExpiresAt);
|
|
14091
14342
|
setAuthSuccess(true);
|
|
14092
14343
|
const deadline = response.emailVerificationDeadline
|
|
14093
14344
|
? new Date(response.emailVerificationDeadline).toLocaleString()
|
|
@@ -14149,7 +14400,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14149
14400
|
setLoading(false);
|
|
14150
14401
|
return;
|
|
14151
14402
|
}
|
|
14152
|
-
await auth.login(response.token, response.user, response.accountData, false, getExpirationFromResponse(response));
|
|
14403
|
+
await auth.login(response.token, response.user, response.accountData, false, getExpirationFromResponse(response), response.refreshToken, response.refreshTokenExpiresAt);
|
|
14153
14404
|
setAuthSuccess(true);
|
|
14154
14405
|
setSuccessMessage('Login successful!');
|
|
14155
14406
|
onAuthSuccess(response.token, response.user, response.accountData);
|
|
@@ -14279,6 +14530,55 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14279
14530
|
}
|
|
14280
14531
|
});
|
|
14281
14532
|
};
|
|
14533
|
+
// Finish a login once a backend AuthResponse has been obtained (shared by the
|
|
14534
|
+
// native adapter paths for Google and Apple).
|
|
14535
|
+
const completeNativeLogin = async (authResponse, successLabel) => {
|
|
14536
|
+
if (!authResponse.token) {
|
|
14537
|
+
throw new Error('Authentication failed - no token received');
|
|
14538
|
+
}
|
|
14539
|
+
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse));
|
|
14540
|
+
setAuthSuccess(true);
|
|
14541
|
+
setSuccessMessage(successLabel);
|
|
14542
|
+
onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
|
|
14543
|
+
};
|
|
14544
|
+
// Sign in with Apple. Apple is native-only in this kit today: it requires an
|
|
14545
|
+
// injected `nativeAuth` adapter (e.g. a Capacitor plugin) that resolves an
|
|
14546
|
+
// Apple identity token. There is no in-browser fallback yet.
|
|
14547
|
+
const handleAppleLogin = async () => {
|
|
14548
|
+
if (!nativeAuth?.signInWithApple) {
|
|
14549
|
+
log.warn('Apple sign-in requested but no nativeAuth.signInWithApple adapter is configured');
|
|
14550
|
+
setError('Apple Sign-In is not available in this environment.');
|
|
14551
|
+
onAuthError?.(new Error('No nativeAuth.signInWithApple adapter configured'));
|
|
14552
|
+
return;
|
|
14553
|
+
}
|
|
14554
|
+
setLoading(true);
|
|
14555
|
+
setError(undefined);
|
|
14556
|
+
try {
|
|
14557
|
+
log.log('Using native adapter for Apple Sign-In');
|
|
14558
|
+
const result = await nativeAuth.signInWithApple({
|
|
14559
|
+
clientId: config?.appleClientId,
|
|
14560
|
+
scopes: ['name', 'email'],
|
|
14561
|
+
});
|
|
14562
|
+
if (!result?.idToken) {
|
|
14563
|
+
throw new Error('Apple Sign-In did not return an identity token');
|
|
14564
|
+
}
|
|
14565
|
+
const authResponse = await api.loginWithApple(result.idToken, {
|
|
14566
|
+
authorizationCode: result.authorizationCode,
|
|
14567
|
+
nonce: result.nonce,
|
|
14568
|
+
appleUserInfo: result.email || result.name ? { email: result.email, name: result.name } : undefined,
|
|
14569
|
+
});
|
|
14570
|
+
log.log('Native Apple login response:', { hasToken: !!authResponse.token, isNewUser: authResponse.isNewUser });
|
|
14571
|
+
await completeNativeLogin(authResponse, 'Apple login successful!');
|
|
14572
|
+
}
|
|
14573
|
+
catch (err) {
|
|
14574
|
+
log.error('Apple Sign-In failed:', err);
|
|
14575
|
+
setError(getFriendlyErrorMessage(err));
|
|
14576
|
+
onAuthError?.(err instanceof Error ? err : new Error(getFriendlyErrorMessage(err)));
|
|
14577
|
+
}
|
|
14578
|
+
finally {
|
|
14579
|
+
setLoading(false);
|
|
14580
|
+
}
|
|
14581
|
+
};
|
|
14282
14582
|
const handleGoogleLogin = async () => {
|
|
14283
14583
|
const hasCustomGoogleClientId = !!config?.googleClientId;
|
|
14284
14584
|
const googleClientId = config?.googleClientId || DEFAULT_GOOGLE_CLIENT_ID;
|
|
@@ -14308,6 +14608,32 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14308
14608
|
setLoading(true);
|
|
14309
14609
|
setError(undefined);
|
|
14310
14610
|
try {
|
|
14611
|
+
// Priority 0: injected Promise-based native adapter (Capacitor, etc.).
|
|
14612
|
+
// This is the clean path — await the plugin directly, no callback bridge.
|
|
14613
|
+
if (nativeAuth?.signInWithGoogle) {
|
|
14614
|
+
log.log('Using injected nativeAuth adapter for Google Sign-In');
|
|
14615
|
+
try {
|
|
14616
|
+
const result = await nativeAuth.signInWithGoogle({
|
|
14617
|
+
serverClientId: googleClientId,
|
|
14618
|
+
scopes: ['email', 'profile'],
|
|
14619
|
+
});
|
|
14620
|
+
if (!result?.idToken) {
|
|
14621
|
+
throw new Error('Google Sign-In did not return an ID token');
|
|
14622
|
+
}
|
|
14623
|
+
const authResponse = await api.loginWithGoogle(result.idToken);
|
|
14624
|
+
log.log('Native (adapter) Google login response:', { hasToken: !!authResponse.token, isNewUser: authResponse.isNewUser });
|
|
14625
|
+
await completeNativeLogin(authResponse, 'Google login successful!');
|
|
14626
|
+
}
|
|
14627
|
+
catch (err) {
|
|
14628
|
+
log.error('Adapter Google Sign-In failed:', err);
|
|
14629
|
+
setError(getFriendlyErrorMessage(err));
|
|
14630
|
+
onAuthError?.(err instanceof Error ? err : new Error(getFriendlyErrorMessage(err)));
|
|
14631
|
+
}
|
|
14632
|
+
finally {
|
|
14633
|
+
setLoading(false);
|
|
14634
|
+
}
|
|
14635
|
+
return;
|
|
14636
|
+
}
|
|
14311
14637
|
if (nativeBridge) {
|
|
14312
14638
|
log.log('Using native bridge for Google Sign-In');
|
|
14313
14639
|
const callbackId = `google_auth_${Date.now()}`;
|
|
@@ -14326,7 +14652,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14326
14652
|
const authResponse = await api.loginWithGoogle(result.idToken);
|
|
14327
14653
|
log.log('Native Google login response:', { hasToken: !!authResponse.token, hasUser: !!authResponse.user, isNewUser: authResponse.isNewUser });
|
|
14328
14654
|
if (authResponse.token) {
|
|
14329
|
-
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse));
|
|
14655
|
+
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse), authResponse.refreshToken, authResponse.refreshTokenExpiresAt);
|
|
14330
14656
|
setAuthSuccess(true);
|
|
14331
14657
|
setSuccessMessage('Google login successful!');
|
|
14332
14658
|
onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
|
|
@@ -14546,7 +14872,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14546
14872
|
log.log('Popup Google login response:', { hasToken: !!authResponse.token, hasUser: !!authResponse.user, isNewUser: authResponse.isNewUser });
|
|
14547
14873
|
if (authResponse.token) {
|
|
14548
14874
|
// Google OAuth can be login or signup - use isNewUser flag from backend if available
|
|
14549
|
-
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse));
|
|
14875
|
+
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse), authResponse.refreshToken, authResponse.refreshTokenExpiresAt);
|
|
14550
14876
|
setAuthSuccess(true);
|
|
14551
14877
|
setSuccessMessage('Google login successful!');
|
|
14552
14878
|
onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
|
|
@@ -14596,7 +14922,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14596
14922
|
log.log('OneTap Google login response:', { hasToken: !!authResponse.token, hasUser: !!authResponse.user, isNewUser: authResponse.isNewUser });
|
|
14597
14923
|
if (authResponse.token) {
|
|
14598
14924
|
// Google OAuth can be login or signup - use isNewUser flag from backend if available
|
|
14599
|
-
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse));
|
|
14925
|
+
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse), authResponse.refreshToken, authResponse.refreshTokenExpiresAt);
|
|
14600
14926
|
setAuthSuccess(true);
|
|
14601
14927
|
setSuccessMessage('Google login successful!');
|
|
14602
14928
|
onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
|
|
@@ -14678,7 +15004,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14678
15004
|
// Phone auth is an INTERACTIVE flow (user entered OTP in UI)
|
|
14679
15005
|
// Unlike deep-link flows (email verification, magic link), there's no URL token to clean up
|
|
14680
15006
|
// Do NOT auto-redirect - let the parent app control the next step (profile completion, etc.)
|
|
14681
|
-
await auth.login(response.token, response.user, response.accountData, response.isNewUser, getExpirationFromResponse(response));
|
|
15007
|
+
await auth.login(response.token, response.user, response.accountData, response.isNewUser, getExpirationFromResponse(response), response.refreshToken, response.refreshTokenExpiresAt);
|
|
14682
15008
|
setAuthSuccess(true);
|
|
14683
15009
|
setSuccessMessage('Phone verified! You are now logged in.');
|
|
14684
15010
|
onAuthSuccess(response.token, response.user, response.accountData);
|
|
@@ -14709,9 +15035,22 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14709
15035
|
const effectiveRedirectUrl = getRedirectUrl();
|
|
14710
15036
|
try {
|
|
14711
15037
|
if (resetToken && confirmPassword) {
|
|
14712
|
-
// Complete password reset with token
|
|
14713
|
-
await api.completePasswordReset(resetToken, emailOrPassword);
|
|
14714
|
-
//
|
|
15038
|
+
// Complete password reset (or invite acceptance) with token
|
|
15039
|
+
const completeResponse = await api.completePasswordReset(resetToken, emailOrPassword);
|
|
15040
|
+
// Invite acceptance under verify-auto-login: backend returns a full session.
|
|
15041
|
+
// Adopt it directly — same pattern as verifyEmail / verifyMagicLink — and skip /login.
|
|
15042
|
+
if (completeResponse?.token && completeResponse.user) {
|
|
15043
|
+
log.log('complete-reset returned a session (invite auto-login), adopting it');
|
|
15044
|
+
await auth.login(completeResponse.token, completeResponse.user, completeResponse.accountData, true, getExpirationFromResponse(completeResponse), completeResponse.refreshToken, completeResponse.refreshTokenExpiresAt);
|
|
15045
|
+
setAuthSuccess(true);
|
|
15046
|
+
setSuccessMessage('Welcome! Your account is ready.');
|
|
15047
|
+
onAuthSuccess(completeResponse.token, completeResponse.user, completeResponse.accountData);
|
|
15048
|
+
setResetToken(undefined);
|
|
15049
|
+
setResetEmail(undefined);
|
|
15050
|
+
return;
|
|
15051
|
+
}
|
|
15052
|
+
// Plain password reset: no session returned. Try auto-login with the new password
|
|
15053
|
+
// if we have the email on hand.
|
|
14715
15054
|
if (resetEmail) {
|
|
14716
15055
|
try {
|
|
14717
15056
|
log.log('Auto-login after password reset for:', resetEmail);
|
|
@@ -14968,7 +15307,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14968
15307
|
throw Object.assign(new Error(resultErrorMessage), session);
|
|
14969
15308
|
}
|
|
14970
15309
|
if (session?.token && session.user) {
|
|
14971
|
-
await auth.login(session.token, session.user, session.accountData, true, getExpirationFromResponse(session));
|
|
15310
|
+
await auth.login(session.token, session.user, session.accountData, true, getExpirationFromResponse(session), session.refreshToken, session.refreshTokenExpiresAt);
|
|
14972
15311
|
if (!proxyMode) {
|
|
14973
15312
|
onAuthSuccess(session.token, session.user, session.accountData);
|
|
14974
15313
|
}
|
|
@@ -15298,11 +15637,11 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
15298
15637
|
const hasEmailProvider = actualProviders.includes('email');
|
|
15299
15638
|
// If email provider is not enabled, only show provider buttons (no email/password form)
|
|
15300
15639
|
if (!hasEmailProvider) {
|
|
15301
|
-
return (jsxRuntime.jsx(ProviderButtons, { enabledProviders: actualProviders, providerOrder: providerOrder, onGoogleLogin: handleGoogleLogin, onPhoneLogin: () => setMode('phone'), onMagicLinkLogin: () => setMode('magic-link'), onWhatsAppLogin: () => setMode('whatsapp'), loading: loading }));
|
|
15640
|
+
return (jsxRuntime.jsx(ProviderButtons, { enabledProviders: actualProviders, providerOrder: providerOrder, onGoogleLogin: handleGoogleLogin, onAppleLogin: handleAppleLogin, onPhoneLogin: () => setMode('phone'), onMagicLinkLogin: () => setMode('magic-link'), onWhatsAppLogin: () => setMode('whatsapp'), loading: loading }));
|
|
15302
15641
|
}
|
|
15303
15642
|
// Button mode: show provider selection first, then email form if email is selected
|
|
15304
15643
|
if (emailDisplayMode === 'button' && !showEmailForm) {
|
|
15305
|
-
return (jsxRuntime.jsx(ProviderButtons, { enabledProviders: actualProviders, providerOrder: providerOrder, onEmailLogin: () => setShowEmailForm(true), onGoogleLogin: handleGoogleLogin, onPhoneLogin: () => setMode('phone'), onMagicLinkLogin: () => setMode('magic-link'), onWhatsAppLogin: () => setMode('whatsapp'), loading: loading }));
|
|
15644
|
+
return (jsxRuntime.jsx(ProviderButtons, { enabledProviders: actualProviders, providerOrder: providerOrder, onEmailLogin: () => setShowEmailForm(true), onGoogleLogin: handleGoogleLogin, onAppleLogin: handleAppleLogin, onPhoneLogin: () => setMode('phone'), onMagicLinkLogin: () => setMode('magic-link'), onWhatsAppLogin: () => setMode('whatsapp'), loading: loading }));
|
|
15306
15645
|
}
|
|
15307
15646
|
// Form mode or email button was clicked: show email form with other providers
|
|
15308
15647
|
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [emailDisplayMode === 'button' && showEmailForm && (jsxRuntime.jsx("button", { onClick: () => setShowEmailForm(false), style: {
|
|
@@ -15321,7 +15660,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
15321
15660
|
setShowResendVerification(false);
|
|
15322
15661
|
setShowRequestNewReset(false);
|
|
15323
15662
|
setError(undefined);
|
|
15324
|
-
}, onForgotPassword: () => setMode('reset-password'), loading: loading, error: error, signupProminence: resolvedSignupProminence, signupRedirectUrl: config?.signupRedirectUrl || customization?.signupRedirectUrl, schema: contactSchema, registrationFieldsConfig: config?.registrationFields, additionalFields: config?.signupAdditionalFields }), emailDisplayMode === 'form' && actualProviders.length > 1 && (jsxRuntime.jsx(ProviderButtons, { enabledProviders: actualProviders.filter((p) => p !== 'email'), providerOrder: providerOrder, onGoogleLogin: handleGoogleLogin, onPhoneLogin: () => setMode('phone'), onMagicLinkLogin: () => setMode('magic-link'), onWhatsAppLogin: () => setMode('whatsapp'), loading: loading }))] }));
|
|
15663
|
+
}, onForgotPassword: () => setMode('reset-password'), loading: loading, error: error, signupProminence: resolvedSignupProminence, signupRedirectUrl: config?.signupRedirectUrl || customization?.signupRedirectUrl, schema: contactSchema, registrationFieldsConfig: config?.registrationFields, additionalFields: config?.signupAdditionalFields }), emailDisplayMode === 'form' && actualProviders.length > 1 && (jsxRuntime.jsx(ProviderButtons, { enabledProviders: actualProviders.filter((p) => p !== 'email'), providerOrder: providerOrder, onGoogleLogin: handleGoogleLogin, onAppleLogin: handleAppleLogin, onPhoneLogin: () => setMode('phone'), onMagicLinkLogin: () => setMode('magic-link'), onWhatsAppLogin: () => setMode('whatsapp'), loading: loading }))] }));
|
|
15325
15664
|
})()] })) })) : null }));
|
|
15326
15665
|
};
|
|
15327
15666
|
|
|
@@ -16462,6 +16801,12 @@ async function setDefaultAuthKitId(collectionId, authKitId) {
|
|
|
16462
16801
|
});
|
|
16463
16802
|
}
|
|
16464
16803
|
|
|
16804
|
+
var defaultAuthKit = /*#__PURE__*/Object.freeze({
|
|
16805
|
+
__proto__: null,
|
|
16806
|
+
getDefaultAuthKitId: getDefaultAuthKitId,
|
|
16807
|
+
setDefaultAuthKitId: setDefaultAuthKitId
|
|
16808
|
+
});
|
|
16809
|
+
|
|
16465
16810
|
exports.AccountManagement = AccountManagement;
|
|
16466
16811
|
exports.AuthProvider = AuthProvider;
|
|
16467
16812
|
exports.AuthUIPreview = AuthUIPreview;
|
|
@@ -16485,6 +16830,7 @@ exports.isRateLimitError = isRateLimitError;
|
|
|
16485
16830
|
exports.isServerError = isServerError;
|
|
16486
16831
|
exports.resolveFields = resolveFields;
|
|
16487
16832
|
exports.setDefaultAuthKitId = setDefaultAuthKitId;
|
|
16833
|
+
exports.setStorageAdapter = setStorageAdapter;
|
|
16488
16834
|
exports.sortFieldsByPlacement = sortFieldsByPlacement;
|
|
16489
16835
|
exports.tokenStorage = tokenStorage;
|
|
16490
16836
|
exports.useAdminDetection = useAdminDetection;
|