@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.esm.js
CHANGED
|
@@ -316,7 +316,7 @@ const EmailAuthForm = ({ mode, onSubmit, onModeSwitch, onForgotPassword, loading
|
|
|
316
316
|
}, disabled: loading })), jsxs("div", { className: "auth-form-header", children: [jsx("h2", { className: "auth-form-title", children: title }), jsx("p", { className: "auth-form-subtitle", children: subtitle })] }), error && (jsxs("div", { className: "auth-error", role: "alert", children: [jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "currentColor", children: 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' && (jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "displayName", className: "auth-label", children: "Full Name" }), 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" })] })), jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "email", className: "auth-label", children: "Email address" }), 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" })] }), jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "password", className: "auth-label", children: "Password" }), 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 && (jsxs(Fragment, { children: [jsx("div", { className: "auth-divider", style: { margin: '16px 0' }, children: jsx("span", { children: "Additional Information" }) }), schemaFields.postCredentials.map(renderSchemaField)] })), mode === 'login' && (jsx("div", { className: "auth-form-footer", children: jsx("button", { type: "button", className: "auth-link", onClick: onForgotPassword, disabled: loading, children: "Forgot password?" }) })), jsx("button", { type: "submit", className: "auth-button auth-button-primary", disabled: loading, children: loading ? (jsx("span", { className: "auth-spinner" })) : mode === 'login' ? ('Sign in') : ('Create account') }), signupProminence !== 'balanced' && signupProminence !== 'none' && (jsxs("div", { className: "auth-divider", children: [jsx("span", { children: mode === 'login' ? "Don't have an account?" : 'Already have an account?' }), signupRedirectUrl && mode === 'login' ? (jsx("a", { href: signupRedirectUrl, target: "_top", className: "auth-link auth-link-bold", children: "Sign up" })) : (jsx("button", { type: "button", className: "auth-link auth-link-bold", onClick: onModeSwitch, disabled: loading, children: mode === 'login' ? 'Sign up' : 'Sign in' }))] }))] }));
|
|
317
317
|
};
|
|
318
318
|
|
|
319
|
-
const ProviderButtons = ({ enabledProviders, providerOrder, onEmailLogin, onGoogleLogin, onPhoneLogin, onMagicLinkLogin, onWhatsAppLogin, loading, }) => {
|
|
319
|
+
const ProviderButtons = ({ enabledProviders, providerOrder, onEmailLogin, onGoogleLogin, onAppleLogin, onPhoneLogin, onMagicLinkLogin, onWhatsAppLogin, loading, }) => {
|
|
320
320
|
if (enabledProviders.length === 0)
|
|
321
321
|
return null;
|
|
322
322
|
// Determine the order of providers to display
|
|
@@ -339,6 +339,11 @@ const ProviderButtons = ({ enabledProviders, providerOrder, onEmailLogin, onGoog
|
|
|
339
339
|
icon: (jsxs("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", children: [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" }), 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" }), 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" }), 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" })] })),
|
|
340
340
|
onClick: onGoogleLogin
|
|
341
341
|
},
|
|
342
|
+
apple: {
|
|
343
|
+
label: 'Continue with Apple',
|
|
344
|
+
icon: (jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true", children: 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" }) })),
|
|
345
|
+
onClick: () => onAppleLogin?.()
|
|
346
|
+
},
|
|
342
347
|
phone: {
|
|
343
348
|
label: 'Continue with Phone',
|
|
344
349
|
icon: (jsx("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", children: 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" }) })),
|
|
@@ -11410,6 +11415,25 @@ class AuthAPI {
|
|
|
11410
11415
|
redirectUri,
|
|
11411
11416
|
});
|
|
11412
11417
|
}
|
|
11418
|
+
/**
|
|
11419
|
+
* Sign in with Apple via an identity token (from native "Sign in with Apple"
|
|
11420
|
+
* or the web JS flow). The backend verifies the JWT against Apple's keys.
|
|
11421
|
+
*
|
|
11422
|
+
* Delegates to `smartlinks.authKit.appleLogin`, which on success stores the
|
|
11423
|
+
* bearer token automatically and invalidates the SDK cache.
|
|
11424
|
+
*/
|
|
11425
|
+
async loginWithApple(identityToken, options) {
|
|
11426
|
+
this.log.log('loginWithApple called:', {
|
|
11427
|
+
tokenLength: identityToken?.length,
|
|
11428
|
+
hasAuthCode: !!options?.authorizationCode,
|
|
11429
|
+
hasUserInfo: !!options?.appleUserInfo,
|
|
11430
|
+
});
|
|
11431
|
+
return smartlinks.authKit.appleLogin(this.clientId, identityToken, {
|
|
11432
|
+
authorizationCode: options?.authorizationCode,
|
|
11433
|
+
nonce: options?.nonce,
|
|
11434
|
+
userInfo: options?.appleUserInfo,
|
|
11435
|
+
});
|
|
11436
|
+
}
|
|
11413
11437
|
async sendPhoneCode(phoneNumber) {
|
|
11414
11438
|
return smartlinks.authKit.sendPhoneCode(this.clientId, phoneNumber);
|
|
11415
11439
|
}
|
|
@@ -11458,6 +11482,17 @@ class AuthAPI {
|
|
|
11458
11482
|
throw error;
|
|
11459
11483
|
}
|
|
11460
11484
|
}
|
|
11485
|
+
/**
|
|
11486
|
+
* Complete a password reset OR accept an invite by setting the first password.
|
|
11487
|
+
*
|
|
11488
|
+
* The backend distinguishes the two flows via the token's `metadata.invitedBy`:
|
|
11489
|
+
* - Plain password reset → returns `{ success, message }` only.
|
|
11490
|
+
* - Invite acceptance under `verify-auto-login` → returns a full session
|
|
11491
|
+
* (`token`, `user`, `accountData`, and on native: refresh-token fields)
|
|
11492
|
+
* so the kit can log the user straight in without bouncing them to /login.
|
|
11493
|
+
*
|
|
11494
|
+
* See: SDK_AUTHKIT_REFRESH_TOKENS / "Invite Auto-Login" spec.
|
|
11495
|
+
*/
|
|
11461
11496
|
async completePasswordReset(token, newPassword) {
|
|
11462
11497
|
return smartlinks.authKit.completePasswordReset(this.clientId, token, newPassword);
|
|
11463
11498
|
}
|
|
@@ -11940,6 +11975,24 @@ async function getStorage() {
|
|
|
11940
11975
|
storageInstance = await storageInitPromise;
|
|
11941
11976
|
return storageInstance;
|
|
11942
11977
|
}
|
|
11978
|
+
/**
|
|
11979
|
+
* Install a custom storage backend (e.g. Capacitor Keychain / Keystore).
|
|
11980
|
+
*
|
|
11981
|
+
* Must be called BEFORE `getStorage()` is invoked for the first time
|
|
11982
|
+
* (typically right after your app boots, before `<SmartlinksAuthUI />` mounts).
|
|
11983
|
+
* The supplied adapter then handles ALL token, user, and account persistence
|
|
11984
|
+
* — replacing the default IndexedDB → localStorage → in-memory chain.
|
|
11985
|
+
*
|
|
11986
|
+
* Intended for native shells (Capacitor / Capgo) that want tokens stored in
|
|
11987
|
+
* the OS secure enclave (Keychain on iOS, Keystore on Android) instead of the
|
|
11988
|
+
* WebView's IndexedDB. Implement the `PersistentStorage` interface against
|
|
11989
|
+
* your secure-storage plugin and pass the instance here.
|
|
11990
|
+
*/
|
|
11991
|
+
function setStorageAdapter(adapter) {
|
|
11992
|
+
StorageFactory.reset();
|
|
11993
|
+
storageInstance = adapter;
|
|
11994
|
+
storageInitPromise = Promise.resolve(adapter);
|
|
11995
|
+
}
|
|
11943
11996
|
/**
|
|
11944
11997
|
* Listen for storage changes from other tabs
|
|
11945
11998
|
*/
|
|
@@ -11958,6 +12011,7 @@ function onStorageChange(callback) {
|
|
|
11958
12011
|
}
|
|
11959
12012
|
|
|
11960
12013
|
const TOKEN_KEY = 'token';
|
|
12014
|
+
const REFRESH_TOKEN_KEY = 'refresh_token';
|
|
11961
12015
|
const USER_KEY = 'user';
|
|
11962
12016
|
const ACCOUNT_DATA_KEY = 'account_data';
|
|
11963
12017
|
const ACCOUNT_INFO_KEY = 'account_info';
|
|
@@ -11996,6 +12050,28 @@ const tokenStorage = {
|
|
|
11996
12050
|
const storage = await getStorage();
|
|
11997
12051
|
await storage.removeItem(TOKEN_KEY);
|
|
11998
12052
|
},
|
|
12053
|
+
// ----- Refresh token (native / long-lived sessions) ------------------
|
|
12054
|
+
async saveRefreshToken(token, expiresAt) {
|
|
12055
|
+
const storage = await getStorage();
|
|
12056
|
+
const payload = { token, expiresAt };
|
|
12057
|
+
await storage.setItem(REFRESH_TOKEN_KEY, payload);
|
|
12058
|
+
},
|
|
12059
|
+
async getRefreshToken() {
|
|
12060
|
+
const storage = await getStorage();
|
|
12061
|
+
const rt = await storage.getItem(REFRESH_TOKEN_KEY);
|
|
12062
|
+
if (!rt)
|
|
12063
|
+
return null;
|
|
12064
|
+
if (rt.expiresAt && rt.expiresAt < Date.now()) {
|
|
12065
|
+
console.log('[TokenStorage] Refresh token expired - clearing');
|
|
12066
|
+
await this.clearRefreshToken();
|
|
12067
|
+
return null;
|
|
12068
|
+
}
|
|
12069
|
+
return rt;
|
|
12070
|
+
},
|
|
12071
|
+
async clearRefreshToken() {
|
|
12072
|
+
const storage = await getStorage();
|
|
12073
|
+
await storage.removeItem(REFRESH_TOKEN_KEY);
|
|
12074
|
+
},
|
|
11999
12075
|
async saveUser(user) {
|
|
12000
12076
|
const storage = await getStorage();
|
|
12001
12077
|
await storage.setItem(USER_KEY, user);
|
|
@@ -12010,6 +12086,7 @@ const tokenStorage = {
|
|
|
12010
12086
|
},
|
|
12011
12087
|
async clearAll() {
|
|
12012
12088
|
await this.clearToken();
|
|
12089
|
+
await this.clearRefreshToken();
|
|
12013
12090
|
await this.clearUser();
|
|
12014
12091
|
await this.clearAccountData();
|
|
12015
12092
|
await this.clearAccountInfo();
|
|
@@ -12067,12 +12144,18 @@ const tokenStorage = {
|
|
|
12067
12144
|
|
|
12068
12145
|
// Export context for optional usage (e.g., SmartlinksFrame can work without AuthProvider)
|
|
12069
12146
|
const AuthContext = createContext(undefined);
|
|
12070
|
-
const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 * 1000, preloadAccountInfo = false,
|
|
12147
|
+
const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 * 1000, preloadAccountInfo = false, clientId: clientIdProp,
|
|
12071
12148
|
// Token refresh settings
|
|
12072
12149
|
enableAutoRefresh = true, refreshThresholdPercent = 75, // Refresh when 75% of token lifetime has passed
|
|
12073
12150
|
refreshCheckInterval = 60 * 1000, // Check every minute
|
|
12151
|
+
refreshOnResume,
|
|
12074
12152
|
// Contact & Interaction features
|
|
12075
12153
|
collectionId, enableContactSync, enableInteractionTracking, interactionAppId, interactionConfig, }) => {
|
|
12154
|
+
// Resolved client ID — explicit prop wins; otherwise fall back to the
|
|
12155
|
+
// collection's defaultAuthKitId (the standard pattern — see
|
|
12156
|
+
// mem://architecture/default-authkit-collection-property).
|
|
12157
|
+
const [resolvedClientId, setResolvedClientId] = useState(clientIdProp);
|
|
12158
|
+
const clientId = resolvedClientId;
|
|
12076
12159
|
const [user, setUser] = useState(null);
|
|
12077
12160
|
const [token, setToken] = useState(null);
|
|
12078
12161
|
const [accountData, setAccountData] = useState(null);
|
|
@@ -12088,8 +12171,8 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
12088
12171
|
const pendingVerificationRef = useRef(false);
|
|
12089
12172
|
const proxyRefreshInFlightRef = useRef(false);
|
|
12090
12173
|
// Stable refs for callbacks to avoid useEffect dependency cycles
|
|
12091
|
-
const syncContactRef = useRef();
|
|
12092
|
-
const trackInteractionRef = useRef();
|
|
12174
|
+
const syncContactRef = useRef(undefined);
|
|
12175
|
+
const trackInteractionRef = useRef(undefined);
|
|
12093
12176
|
// Default to enabled if collectionId is provided
|
|
12094
12177
|
const shouldSyncContacts = enableContactSync ?? !!collectionId;
|
|
12095
12178
|
const shouldTrackInteractions = enableInteractionTracking ?? !!collectionId;
|
|
@@ -12772,9 +12855,7 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
12772
12855
|
});
|
|
12773
12856
|
});
|
|
12774
12857
|
};
|
|
12775
|
-
const login = useCallback(async (authToken, authUser, authAccountData, isNewUser, expiresAt
|
|
12776
|
-
// Note: waitForParentAck removed - we ALWAYS wait for parent ack in iframe mode now
|
|
12777
|
-
) => {
|
|
12858
|
+
const login = useCallback(async (authToken, authUser, authAccountData, isNewUser, expiresAt, refreshTokenValue, refreshTokenExpiresAt) => {
|
|
12778
12859
|
try {
|
|
12779
12860
|
// Only persist to storage in standalone mode
|
|
12780
12861
|
if (!proxyMode) {
|
|
@@ -12788,6 +12869,15 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
12788
12869
|
else {
|
|
12789
12870
|
await tokenStorage.clearAccountData();
|
|
12790
12871
|
}
|
|
12872
|
+
// Persist refresh token when the backend issued one (native clients).
|
|
12873
|
+
if (refreshTokenValue) {
|
|
12874
|
+
const rtExp = refreshTokenExpiresAt
|
|
12875
|
+
?? Date.now() + 90 * 24 * 60 * 60 * 1000; // 90d fallback per spec
|
|
12876
|
+
await tokenStorage.saveRefreshToken(refreshTokenValue, rtExp);
|
|
12877
|
+
}
|
|
12878
|
+
else {
|
|
12879
|
+
await tokenStorage.clearRefreshToken();
|
|
12880
|
+
}
|
|
12791
12881
|
smartlinks.auth.verifyToken(authToken).catch(() => { });
|
|
12792
12882
|
}
|
|
12793
12883
|
// Always update memory state (but NOT isVerified yet - wait for parent ack in iframe mode)
|
|
@@ -12851,6 +12941,17 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
12851
12941
|
}
|
|
12852
12942
|
// Only clear persistent storage in standalone mode
|
|
12853
12943
|
if (!proxyMode) {
|
|
12944
|
+
// Best-effort: revoke the refresh-token family server-side before
|
|
12945
|
+
// wiping local state, so a stolen copy is dead immediately.
|
|
12946
|
+
try {
|
|
12947
|
+
const storedRefresh = await tokenStorage.getRefreshToken();
|
|
12948
|
+
if (storedRefresh?.token && clientId) {
|
|
12949
|
+
await smartlinks.authKit.logout(clientId, storedRefresh.token);
|
|
12950
|
+
}
|
|
12951
|
+
}
|
|
12952
|
+
catch (err) {
|
|
12953
|
+
console.warn('[AuthContext] Refresh-token revoke failed (continuing):', err);
|
|
12954
|
+
}
|
|
12854
12955
|
await tokenStorage.clearAll();
|
|
12855
12956
|
smartlinks.auth.logout();
|
|
12856
12957
|
}
|
|
@@ -12876,7 +12977,7 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
12876
12977
|
catch (error) {
|
|
12877
12978
|
console.error('Failed to clear auth data from storage:', error);
|
|
12878
12979
|
}
|
|
12879
|
-
}, [proxyMode, notifyAuthStateChange, user, contactId, collectionId, shouldTrackInteractions, trackInteraction]);
|
|
12980
|
+
}, [proxyMode, clientId, notifyAuthStateChange, user, contactId, collectionId, shouldTrackInteractions, trackInteraction]);
|
|
12880
12981
|
const getToken = useCallback(async () => {
|
|
12881
12982
|
if (proxyMode) {
|
|
12882
12983
|
return token;
|
|
@@ -12903,12 +13004,44 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
12903
13004
|
expiresIn: Math.max(0, storedToken.expiresAt - Date.now()),
|
|
12904
13005
|
};
|
|
12905
13006
|
}, [proxyMode, token]);
|
|
12906
|
-
// Refresh token -
|
|
13007
|
+
// Refresh token - prefers the AuthKit refresh-token endpoint when a refresh
|
|
13008
|
+
// token is stored (native clients); otherwise falls back to verify-and-extend
|
|
13009
|
+
// for web clients whose backend still issues only short-lived JWTs.
|
|
12907
13010
|
const refreshToken = useCallback(async () => {
|
|
12908
13011
|
if (proxyMode) {
|
|
12909
13012
|
throw new Error('Token refresh in proxy mode is handled by the parent application');
|
|
12910
13013
|
}
|
|
12911
13014
|
const storedToken = await tokenStorage.getToken();
|
|
13015
|
+
const storedRefresh = await tokenStorage.getRefreshToken();
|
|
13016
|
+
// -------- Path A: true refresh-token rotation (native) ---------------
|
|
13017
|
+
if (storedRefresh?.token && clientId) {
|
|
13018
|
+
try {
|
|
13019
|
+
const resp = await smartlinks.authKit.refreshToken(clientId, storedRefresh.token);
|
|
13020
|
+
await tokenStorage.saveToken(resp.token, resp.expiresAt);
|
|
13021
|
+
await tokenStorage.saveRefreshToken(resp.refreshToken, resp.refreshTokenExpiresAt);
|
|
13022
|
+
setToken(resp.token);
|
|
13023
|
+
setIsVerified(true);
|
|
13024
|
+
pendingVerificationRef.current = false;
|
|
13025
|
+
notifyAuthStateChange('TOKEN_REFRESH', user, resp.token, accountData, accountInfo, true, contact, contactId);
|
|
13026
|
+
return resp.token;
|
|
13027
|
+
}
|
|
13028
|
+
catch (error) {
|
|
13029
|
+
console.error('[AuthContext] Refresh-token rotation failed:', error);
|
|
13030
|
+
if (isNetworkError(error))
|
|
13031
|
+
throw error;
|
|
13032
|
+
// Reuse / invalid / expired → force re-login. The SDK surfaces these
|
|
13033
|
+
// as RefreshErrorCode (INVALID_REFRESH_TOKEN / REUSE_DETECTED / MISSING).
|
|
13034
|
+
const code = error?.errorCode ?? error?.code;
|
|
13035
|
+
if (code === 'INVALID_REFRESH_TOKEN' ||
|
|
13036
|
+
code === 'REFRESH_TOKEN_REUSE_DETECTED' ||
|
|
13037
|
+
code === 'MISSING_REFRESH_TOKEN') {
|
|
13038
|
+
await logout();
|
|
13039
|
+
throw new Error('Session expired. Please login again.');
|
|
13040
|
+
}
|
|
13041
|
+
// Fall through to verify-and-extend on unexpected errors.
|
|
13042
|
+
}
|
|
13043
|
+
}
|
|
13044
|
+
// -------- Path B: legacy verify-and-extend (web) ---------------------
|
|
12912
13045
|
if (!storedToken?.token) {
|
|
12913
13046
|
throw new Error('No token to refresh. Please login first.');
|
|
12914
13047
|
}
|
|
@@ -12933,7 +13066,7 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
12933
13066
|
await logout();
|
|
12934
13067
|
throw new Error('Token refresh failed. Please login again.');
|
|
12935
13068
|
}
|
|
12936
|
-
}, [proxyMode, user, accountData, accountInfo, contact, contactId, notifyAuthStateChange, isNetworkError, logout]);
|
|
13069
|
+
}, [proxyMode, clientId, user, accountData, accountInfo, contact, contactId, notifyAuthStateChange, isNetworkError, logout]);
|
|
12937
13070
|
const getAccount = useCallback(async (forceRefresh = false) => {
|
|
12938
13071
|
try {
|
|
12939
13072
|
if (proxyMode) {
|
|
@@ -13097,6 +13230,105 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
13097
13230
|
clearInterval(intervalId);
|
|
13098
13231
|
};
|
|
13099
13232
|
}, [proxyMode, enableAutoRefresh, refreshCheckInterval, refreshThresholdPercent, token, user, refreshToken]);
|
|
13233
|
+
// Resolve clientId from the collection's defaultAuthKitId when not provided
|
|
13234
|
+
// explicitly. Most apps only pass `collectionId` — this means refresh-token
|
|
13235
|
+
// rotation and server-side logout revocation still work end-to-end.
|
|
13236
|
+
useEffect(() => {
|
|
13237
|
+
if (clientIdProp) {
|
|
13238
|
+
setResolvedClientId(clientIdProp);
|
|
13239
|
+
return;
|
|
13240
|
+
}
|
|
13241
|
+
if (!collectionId)
|
|
13242
|
+
return;
|
|
13243
|
+
let cancelled = false;
|
|
13244
|
+
(async () => {
|
|
13245
|
+
try {
|
|
13246
|
+
const { getDefaultAuthKitId } = await Promise.resolve().then(function () { return defaultAuthKit; });
|
|
13247
|
+
const id = await getDefaultAuthKitId(collectionId);
|
|
13248
|
+
if (!cancelled && id)
|
|
13249
|
+
setResolvedClientId(id);
|
|
13250
|
+
}
|
|
13251
|
+
catch (err) {
|
|
13252
|
+
console.warn('[AuthContext] Could not resolve default authKitId from collection:', err);
|
|
13253
|
+
}
|
|
13254
|
+
})();
|
|
13255
|
+
return () => { cancelled = true; };
|
|
13256
|
+
}, [clientIdProp, collectionId]);
|
|
13257
|
+
// Refresh-on-resume: fires when the app returns to the foreground.
|
|
13258
|
+
// - Capacitor: `App.addListener('appStateChange')` via dynamic import (no
|
|
13259
|
+
// runtime dep on web).
|
|
13260
|
+
// - Universal fallback: `document.visibilitychange` — also catches PWAs /
|
|
13261
|
+
// web tabs that have been backgrounded for hours.
|
|
13262
|
+
useEffect(() => {
|
|
13263
|
+
if (proxyMode || !enableAutoRefresh || !token || !user)
|
|
13264
|
+
return;
|
|
13265
|
+
// Default ON when we detect a native shell; OFF otherwise.
|
|
13266
|
+
const isNative = (() => {
|
|
13267
|
+
try {
|
|
13268
|
+
const cap = window.Capacitor;
|
|
13269
|
+
if (cap?.isNativePlatform?.() === true)
|
|
13270
|
+
return true;
|
|
13271
|
+
if (window.AuthKit?.isNative === true)
|
|
13272
|
+
return true;
|
|
13273
|
+
}
|
|
13274
|
+
catch { /* noop */ }
|
|
13275
|
+
return false;
|
|
13276
|
+
})();
|
|
13277
|
+
const enabled = refreshOnResume ?? isNative;
|
|
13278
|
+
if (!enabled)
|
|
13279
|
+
return;
|
|
13280
|
+
const maybeRefresh = async () => {
|
|
13281
|
+
try {
|
|
13282
|
+
const storedToken = await tokenStorage.getToken();
|
|
13283
|
+
if (!storedToken?.expiresAt)
|
|
13284
|
+
return;
|
|
13285
|
+
const remainingMs = storedToken.expiresAt - Date.now();
|
|
13286
|
+
const totalLifetimeMs = 7 * 24 * 60 * 60 * 1000;
|
|
13287
|
+
const percentUsed = ((totalLifetimeMs - remainingMs) / totalLifetimeMs) * 100;
|
|
13288
|
+
if (percentUsed < refreshThresholdPercent)
|
|
13289
|
+
return;
|
|
13290
|
+
await refreshToken().catch(() => { });
|
|
13291
|
+
}
|
|
13292
|
+
catch (err) {
|
|
13293
|
+
console.error('[AuthContext] Resume refresh check failed:', err);
|
|
13294
|
+
}
|
|
13295
|
+
};
|
|
13296
|
+
// Universal visibility listener
|
|
13297
|
+
const onVisibility = () => {
|
|
13298
|
+
if (document.visibilityState === 'visible')
|
|
13299
|
+
maybeRefresh();
|
|
13300
|
+
};
|
|
13301
|
+
document.addEventListener('visibilitychange', onVisibility);
|
|
13302
|
+
// Capacitor listener (optional)
|
|
13303
|
+
let capRemove;
|
|
13304
|
+
(async () => {
|
|
13305
|
+
try {
|
|
13306
|
+
// Only attempt to load @capacitor/app in an actual native shell.
|
|
13307
|
+
// The specifier is obfuscated so bundlers (Vite/webpack) don't try
|
|
13308
|
+
// to statically resolve an optional peer dependency at build time.
|
|
13309
|
+
const isNative = typeof window !== 'undefined' &&
|
|
13310
|
+
(window.Capacitor?.isNativePlatform?.() === true ||
|
|
13311
|
+
window.AuthKit?.isNative === true);
|
|
13312
|
+
if (!isNative)
|
|
13313
|
+
return;
|
|
13314
|
+
const specifier = ['@capacitor', 'app'].join('/');
|
|
13315
|
+
const dynamicImport = new Function('s', 'return import(s)');
|
|
13316
|
+
const mod = await dynamicImport(specifier);
|
|
13317
|
+
const handle = await mod.App.addListener('appStateChange', (state) => {
|
|
13318
|
+
if (state.isActive)
|
|
13319
|
+
maybeRefresh();
|
|
13320
|
+
});
|
|
13321
|
+
capRemove = () => handle?.remove?.();
|
|
13322
|
+
}
|
|
13323
|
+
catch {
|
|
13324
|
+
// @capacitor/app not installed or unavailable — visibilitychange handles it.
|
|
13325
|
+
}
|
|
13326
|
+
})();
|
|
13327
|
+
return () => {
|
|
13328
|
+
document.removeEventListener('visibilitychange', onVisibility);
|
|
13329
|
+
capRemove?.();
|
|
13330
|
+
};
|
|
13331
|
+
}, [proxyMode, enableAutoRefresh, refreshOnResume, refreshThresholdPercent, token, user, refreshToken]);
|
|
13100
13332
|
const value = {
|
|
13101
13333
|
user,
|
|
13102
13334
|
token,
|
|
@@ -13358,8 +13590,22 @@ const loadGoogleIdentityServices = () => {
|
|
|
13358
13590
|
document.head.appendChild(script);
|
|
13359
13591
|
});
|
|
13360
13592
|
};
|
|
13593
|
+
// Helper to detect a Capacitor native runtime (iOS/Android shell, not the web)
|
|
13594
|
+
const isCapacitorNative = () => {
|
|
13595
|
+
const cap = window.Capacitor;
|
|
13596
|
+
if (!cap)
|
|
13597
|
+
return false;
|
|
13598
|
+
// isNativePlatform() is the modern API; fall back to platform string for older cores
|
|
13599
|
+
if (typeof cap.isNativePlatform === 'function')
|
|
13600
|
+
return cap.isNativePlatform();
|
|
13601
|
+
return cap.platform === 'ios' || cap.platform === 'android';
|
|
13602
|
+
};
|
|
13361
13603
|
// Helper to detect WebView environments (Android/iOS)
|
|
13362
13604
|
const detectWebView = () => {
|
|
13605
|
+
// Capacitor apps run inside a WebView (WKWebView/Android WebView) but don't
|
|
13606
|
+
// always set the `wv` UA token — treat them as WebView explicitly.
|
|
13607
|
+
if (isCapacitorNative())
|
|
13608
|
+
return true;
|
|
13363
13609
|
const ua = navigator.userAgent;
|
|
13364
13610
|
// Android WebView detection
|
|
13365
13611
|
if (/Android/i.test(ua)) {
|
|
@@ -13467,7 +13713,7 @@ const checkSilentGoogleSignIn = async (clientId, googleClientId) => {
|
|
|
13467
13713
|
});
|
|
13468
13714
|
};
|
|
13469
13715
|
// getFriendlyErrorMessage is now imported from ../utils/errorHandling
|
|
13470
|
-
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, }) => {
|
|
13716
|
+
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, }) => {
|
|
13471
13717
|
// Resolve signup prominence from props, customization, config, or default
|
|
13472
13718
|
const resolvedSignupProminence = signupProminence || customization?.signupProminence || 'minimal';
|
|
13473
13719
|
// Determine initial mode based on signupProminence setting
|
|
@@ -13767,23 +14013,28 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13767
14013
|
};
|
|
13768
14014
|
fetchSchema();
|
|
13769
14015
|
}, [collectionId, sdkReady]);
|
|
13770
|
-
// Silent
|
|
13771
|
-
//
|
|
14016
|
+
// Silent Sign-In check on mount (for native apps).
|
|
14017
|
+
// Prefers the injected `nativeAuth.checkSignIn` adapter (Capacitor) when
|
|
14018
|
+
// `enableSilentNativeSignIn` is set; otherwise falls back to the legacy
|
|
14019
|
+
// `window.AuthKit` bridge when `enableSilentGoogleSignIn` is set.
|
|
14020
|
+
const wantsSilentNative = enableSilentNativeSignIn && !!nativeAuth?.checkSignIn;
|
|
13772
14021
|
useEffect(() => {
|
|
13773
|
-
if (!enableSilentGoogleSignIn || silentSignInChecked || !sdkReady || auth.isAuthenticated) {
|
|
14022
|
+
if ((!enableSilentGoogleSignIn && !wantsSilentNative) || silentSignInChecked || !sdkReady || auth.isAuthenticated) {
|
|
13774
14023
|
return;
|
|
13775
14024
|
}
|
|
13776
14025
|
const googleClientId = config?.googleClientId || DEFAULT_GOOGLE_CLIENT_ID;
|
|
13777
14026
|
const performSilentSignIn = async () => {
|
|
13778
14027
|
try {
|
|
13779
|
-
const result =
|
|
14028
|
+
const result = wantsSilentNative
|
|
14029
|
+
? await nativeAuth.checkSignIn({ serverClientId: googleClientId })
|
|
14030
|
+
: await checkSilentGoogleSignIn(clientId, googleClientId);
|
|
13780
14031
|
setSilentSignInChecked(true);
|
|
13781
14032
|
if (result?.isSignedIn && result.idToken) {
|
|
13782
14033
|
setLoading(true);
|
|
13783
14034
|
try {
|
|
13784
14035
|
const authResponse = await api.loginWithGoogle(result.idToken);
|
|
13785
14036
|
if (authResponse.token) {
|
|
13786
|
-
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, false, getExpirationFromResponse(authResponse));
|
|
14037
|
+
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, false, getExpirationFromResponse(authResponse), authResponse.refreshToken, authResponse.refreshTokenExpiresAt);
|
|
13787
14038
|
setAuthSuccess(true);
|
|
13788
14039
|
setSuccessMessage('Signed in automatically with Google!');
|
|
13789
14040
|
onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
|
|
@@ -13802,7 +14053,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13802
14053
|
}
|
|
13803
14054
|
};
|
|
13804
14055
|
performSilentSignIn();
|
|
13805
|
-
}, [enableSilentGoogleSignIn, silentSignInChecked, sdkReady, auth.isAuthenticated, clientId, config?.googleClientId, api, auth, onAuthSuccess]);
|
|
14056
|
+
}, [enableSilentGoogleSignIn, wantsSilentNative, nativeAuth, silentSignInChecked, sdkReady, auth.isAuthenticated, clientId, config?.googleClientId, api, auth, onAuthSuccess]);
|
|
13806
14057
|
// Reset showEmailForm when mode changes away from login/register
|
|
13807
14058
|
useEffect(() => {
|
|
13808
14059
|
if (mode !== 'login' && mode !== 'register') {
|
|
@@ -13878,7 +14129,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13878
14129
|
if ((verificationMode === 'verify-auto-login' || verificationMode === 'immediate') && response.token) {
|
|
13879
14130
|
// Auto-login modes: Log the user in immediately if token is provided
|
|
13880
14131
|
// Always await - auth.login now waits for parent ack automatically in iframe mode
|
|
13881
|
-
await auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response));
|
|
14132
|
+
await auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response), response.refreshToken, response.refreshTokenExpiresAt);
|
|
13882
14133
|
setUrlAuthProcessing(false); // Clear processing state before redirect/success
|
|
13883
14134
|
if (redirectUrl) {
|
|
13884
14135
|
// Redirect to clean URL and resume flow
|
|
@@ -13938,7 +14189,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13938
14189
|
// Auto-login with magic link if token is provided
|
|
13939
14190
|
if (response.token) {
|
|
13940
14191
|
// Always await - auth.login now waits for parent ack automatically in iframe mode
|
|
13941
|
-
await auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response));
|
|
14192
|
+
await auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response), response.refreshToken, response.refreshTokenExpiresAt);
|
|
13942
14193
|
setUrlAuthProcessing(false); // Clear processing state before redirect/success
|
|
13943
14194
|
if (redirectUrl) {
|
|
13944
14195
|
// Redirect to clean URL and resume flow
|
|
@@ -14013,7 +14264,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14013
14264
|
log.log('Google OAuth code exchange response:', { hasToken: !!response.token, hasUser: !!response.user, isNewUser: response.isNewUser });
|
|
14014
14265
|
if (response.token) {
|
|
14015
14266
|
// Await login to ensure token is persisted before any navigation
|
|
14016
|
-
await auth.login(response.token, response.user, response.accountData, response.isNewUser, getExpirationFromResponse(response));
|
|
14267
|
+
await auth.login(response.token, response.user, response.accountData, response.isNewUser, getExpirationFromResponse(response), response.refreshToken, response.refreshTokenExpiresAt);
|
|
14017
14268
|
setAuthSuccess(true);
|
|
14018
14269
|
setSuccessMessage('Google login successful!');
|
|
14019
14270
|
onAuthSuccess(response.token, response.user, response.accountData);
|
|
@@ -14067,7 +14318,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14067
14318
|
// Handle different verification modes
|
|
14068
14319
|
if (verificationMode === 'immediate' && response.token) {
|
|
14069
14320
|
// Immediate mode: Log in right away if token is provided (isNewUser=true for registration)
|
|
14070
|
-
await auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response));
|
|
14321
|
+
await auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response), response.refreshToken, response.refreshTokenExpiresAt);
|
|
14071
14322
|
setAuthSuccess(true);
|
|
14072
14323
|
const deadline = response.emailVerificationDeadline
|
|
14073
14324
|
? new Date(response.emailVerificationDeadline).toLocaleString()
|
|
@@ -14129,7 +14380,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14129
14380
|
setLoading(false);
|
|
14130
14381
|
return;
|
|
14131
14382
|
}
|
|
14132
|
-
await auth.login(response.token, response.user, response.accountData, false, getExpirationFromResponse(response));
|
|
14383
|
+
await auth.login(response.token, response.user, response.accountData, false, getExpirationFromResponse(response), response.refreshToken, response.refreshTokenExpiresAt);
|
|
14133
14384
|
setAuthSuccess(true);
|
|
14134
14385
|
setSuccessMessage('Login successful!');
|
|
14135
14386
|
onAuthSuccess(response.token, response.user, response.accountData);
|
|
@@ -14259,6 +14510,55 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14259
14510
|
}
|
|
14260
14511
|
});
|
|
14261
14512
|
};
|
|
14513
|
+
// Finish a login once a backend AuthResponse has been obtained (shared by the
|
|
14514
|
+
// native adapter paths for Google and Apple).
|
|
14515
|
+
const completeNativeLogin = async (authResponse, successLabel) => {
|
|
14516
|
+
if (!authResponse.token) {
|
|
14517
|
+
throw new Error('Authentication failed - no token received');
|
|
14518
|
+
}
|
|
14519
|
+
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse));
|
|
14520
|
+
setAuthSuccess(true);
|
|
14521
|
+
setSuccessMessage(successLabel);
|
|
14522
|
+
onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
|
|
14523
|
+
};
|
|
14524
|
+
// Sign in with Apple. Apple is native-only in this kit today: it requires an
|
|
14525
|
+
// injected `nativeAuth` adapter (e.g. a Capacitor plugin) that resolves an
|
|
14526
|
+
// Apple identity token. There is no in-browser fallback yet.
|
|
14527
|
+
const handleAppleLogin = async () => {
|
|
14528
|
+
if (!nativeAuth?.signInWithApple) {
|
|
14529
|
+
log.warn('Apple sign-in requested but no nativeAuth.signInWithApple adapter is configured');
|
|
14530
|
+
setError('Apple Sign-In is not available in this environment.');
|
|
14531
|
+
onAuthError?.(new Error('No nativeAuth.signInWithApple adapter configured'));
|
|
14532
|
+
return;
|
|
14533
|
+
}
|
|
14534
|
+
setLoading(true);
|
|
14535
|
+
setError(undefined);
|
|
14536
|
+
try {
|
|
14537
|
+
log.log('Using native adapter for Apple Sign-In');
|
|
14538
|
+
const result = await nativeAuth.signInWithApple({
|
|
14539
|
+
clientId: config?.appleClientId,
|
|
14540
|
+
scopes: ['name', 'email'],
|
|
14541
|
+
});
|
|
14542
|
+
if (!result?.idToken) {
|
|
14543
|
+
throw new Error('Apple Sign-In did not return an identity token');
|
|
14544
|
+
}
|
|
14545
|
+
const authResponse = await api.loginWithApple(result.idToken, {
|
|
14546
|
+
authorizationCode: result.authorizationCode,
|
|
14547
|
+
nonce: result.nonce,
|
|
14548
|
+
appleUserInfo: result.email || result.name ? { email: result.email, name: result.name } : undefined,
|
|
14549
|
+
});
|
|
14550
|
+
log.log('Native Apple login response:', { hasToken: !!authResponse.token, isNewUser: authResponse.isNewUser });
|
|
14551
|
+
await completeNativeLogin(authResponse, 'Apple login successful!');
|
|
14552
|
+
}
|
|
14553
|
+
catch (err) {
|
|
14554
|
+
log.error('Apple Sign-In failed:', err);
|
|
14555
|
+
setError(getFriendlyErrorMessage(err));
|
|
14556
|
+
onAuthError?.(err instanceof Error ? err : new Error(getFriendlyErrorMessage(err)));
|
|
14557
|
+
}
|
|
14558
|
+
finally {
|
|
14559
|
+
setLoading(false);
|
|
14560
|
+
}
|
|
14561
|
+
};
|
|
14262
14562
|
const handleGoogleLogin = async () => {
|
|
14263
14563
|
const hasCustomGoogleClientId = !!config?.googleClientId;
|
|
14264
14564
|
const googleClientId = config?.googleClientId || DEFAULT_GOOGLE_CLIENT_ID;
|
|
@@ -14288,6 +14588,32 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14288
14588
|
setLoading(true);
|
|
14289
14589
|
setError(undefined);
|
|
14290
14590
|
try {
|
|
14591
|
+
// Priority 0: injected Promise-based native adapter (Capacitor, etc.).
|
|
14592
|
+
// This is the clean path — await the plugin directly, no callback bridge.
|
|
14593
|
+
if (nativeAuth?.signInWithGoogle) {
|
|
14594
|
+
log.log('Using injected nativeAuth adapter for Google Sign-In');
|
|
14595
|
+
try {
|
|
14596
|
+
const result = await nativeAuth.signInWithGoogle({
|
|
14597
|
+
serverClientId: googleClientId,
|
|
14598
|
+
scopes: ['email', 'profile'],
|
|
14599
|
+
});
|
|
14600
|
+
if (!result?.idToken) {
|
|
14601
|
+
throw new Error('Google Sign-In did not return an ID token');
|
|
14602
|
+
}
|
|
14603
|
+
const authResponse = await api.loginWithGoogle(result.idToken);
|
|
14604
|
+
log.log('Native (adapter) Google login response:', { hasToken: !!authResponse.token, isNewUser: authResponse.isNewUser });
|
|
14605
|
+
await completeNativeLogin(authResponse, 'Google login successful!');
|
|
14606
|
+
}
|
|
14607
|
+
catch (err) {
|
|
14608
|
+
log.error('Adapter Google Sign-In failed:', err);
|
|
14609
|
+
setError(getFriendlyErrorMessage(err));
|
|
14610
|
+
onAuthError?.(err instanceof Error ? err : new Error(getFriendlyErrorMessage(err)));
|
|
14611
|
+
}
|
|
14612
|
+
finally {
|
|
14613
|
+
setLoading(false);
|
|
14614
|
+
}
|
|
14615
|
+
return;
|
|
14616
|
+
}
|
|
14291
14617
|
if (nativeBridge) {
|
|
14292
14618
|
log.log('Using native bridge for Google Sign-In');
|
|
14293
14619
|
const callbackId = `google_auth_${Date.now()}`;
|
|
@@ -14306,7 +14632,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14306
14632
|
const authResponse = await api.loginWithGoogle(result.idToken);
|
|
14307
14633
|
log.log('Native Google login response:', { hasToken: !!authResponse.token, hasUser: !!authResponse.user, isNewUser: authResponse.isNewUser });
|
|
14308
14634
|
if (authResponse.token) {
|
|
14309
|
-
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse));
|
|
14635
|
+
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse), authResponse.refreshToken, authResponse.refreshTokenExpiresAt);
|
|
14310
14636
|
setAuthSuccess(true);
|
|
14311
14637
|
setSuccessMessage('Google login successful!');
|
|
14312
14638
|
onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
|
|
@@ -14526,7 +14852,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14526
14852
|
log.log('Popup Google login response:', { hasToken: !!authResponse.token, hasUser: !!authResponse.user, isNewUser: authResponse.isNewUser });
|
|
14527
14853
|
if (authResponse.token) {
|
|
14528
14854
|
// Google OAuth can be login or signup - use isNewUser flag from backend if available
|
|
14529
|
-
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse));
|
|
14855
|
+
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse), authResponse.refreshToken, authResponse.refreshTokenExpiresAt);
|
|
14530
14856
|
setAuthSuccess(true);
|
|
14531
14857
|
setSuccessMessage('Google login successful!');
|
|
14532
14858
|
onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
|
|
@@ -14576,7 +14902,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14576
14902
|
log.log('OneTap Google login response:', { hasToken: !!authResponse.token, hasUser: !!authResponse.user, isNewUser: authResponse.isNewUser });
|
|
14577
14903
|
if (authResponse.token) {
|
|
14578
14904
|
// Google OAuth can be login or signup - use isNewUser flag from backend if available
|
|
14579
|
-
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse));
|
|
14905
|
+
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse), authResponse.refreshToken, authResponse.refreshTokenExpiresAt);
|
|
14580
14906
|
setAuthSuccess(true);
|
|
14581
14907
|
setSuccessMessage('Google login successful!');
|
|
14582
14908
|
onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
|
|
@@ -14658,7 +14984,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14658
14984
|
// Phone auth is an INTERACTIVE flow (user entered OTP in UI)
|
|
14659
14985
|
// Unlike deep-link flows (email verification, magic link), there's no URL token to clean up
|
|
14660
14986
|
// Do NOT auto-redirect - let the parent app control the next step (profile completion, etc.)
|
|
14661
|
-
await auth.login(response.token, response.user, response.accountData, response.isNewUser, getExpirationFromResponse(response));
|
|
14987
|
+
await auth.login(response.token, response.user, response.accountData, response.isNewUser, getExpirationFromResponse(response), response.refreshToken, response.refreshTokenExpiresAt);
|
|
14662
14988
|
setAuthSuccess(true);
|
|
14663
14989
|
setSuccessMessage('Phone verified! You are now logged in.');
|
|
14664
14990
|
onAuthSuccess(response.token, response.user, response.accountData);
|
|
@@ -14689,9 +15015,22 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14689
15015
|
const effectiveRedirectUrl = getRedirectUrl();
|
|
14690
15016
|
try {
|
|
14691
15017
|
if (resetToken && confirmPassword) {
|
|
14692
|
-
// Complete password reset with token
|
|
14693
|
-
await api.completePasswordReset(resetToken, emailOrPassword);
|
|
14694
|
-
//
|
|
15018
|
+
// Complete password reset (or invite acceptance) with token
|
|
15019
|
+
const completeResponse = await api.completePasswordReset(resetToken, emailOrPassword);
|
|
15020
|
+
// Invite acceptance under verify-auto-login: backend returns a full session.
|
|
15021
|
+
// Adopt it directly — same pattern as verifyEmail / verifyMagicLink — and skip /login.
|
|
15022
|
+
if (completeResponse?.token && completeResponse.user) {
|
|
15023
|
+
log.log('complete-reset returned a session (invite auto-login), adopting it');
|
|
15024
|
+
await auth.login(completeResponse.token, completeResponse.user, completeResponse.accountData, true, getExpirationFromResponse(completeResponse), completeResponse.refreshToken, completeResponse.refreshTokenExpiresAt);
|
|
15025
|
+
setAuthSuccess(true);
|
|
15026
|
+
setSuccessMessage('Welcome! Your account is ready.');
|
|
15027
|
+
onAuthSuccess(completeResponse.token, completeResponse.user, completeResponse.accountData);
|
|
15028
|
+
setResetToken(undefined);
|
|
15029
|
+
setResetEmail(undefined);
|
|
15030
|
+
return;
|
|
15031
|
+
}
|
|
15032
|
+
// Plain password reset: no session returned. Try auto-login with the new password
|
|
15033
|
+
// if we have the email on hand.
|
|
14695
15034
|
if (resetEmail) {
|
|
14696
15035
|
try {
|
|
14697
15036
|
log.log('Auto-login after password reset for:', resetEmail);
|
|
@@ -14948,7 +15287,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14948
15287
|
throw Object.assign(new Error(resultErrorMessage), session);
|
|
14949
15288
|
}
|
|
14950
15289
|
if (session?.token && session.user) {
|
|
14951
|
-
await auth.login(session.token, session.user, session.accountData, true, getExpirationFromResponse(session));
|
|
15290
|
+
await auth.login(session.token, session.user, session.accountData, true, getExpirationFromResponse(session), session.refreshToken, session.refreshTokenExpiresAt);
|
|
14952
15291
|
if (!proxyMode) {
|
|
14953
15292
|
onAuthSuccess(session.token, session.user, session.accountData);
|
|
14954
15293
|
}
|
|
@@ -15278,11 +15617,11 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
15278
15617
|
const hasEmailProvider = actualProviders.includes('email');
|
|
15279
15618
|
// If email provider is not enabled, only show provider buttons (no email/password form)
|
|
15280
15619
|
if (!hasEmailProvider) {
|
|
15281
|
-
return (jsx(ProviderButtons, { enabledProviders: actualProviders, providerOrder: providerOrder, onGoogleLogin: handleGoogleLogin, onPhoneLogin: () => setMode('phone'), onMagicLinkLogin: () => setMode('magic-link'), onWhatsAppLogin: () => setMode('whatsapp'), loading: loading }));
|
|
15620
|
+
return (jsx(ProviderButtons, { enabledProviders: actualProviders, providerOrder: providerOrder, onGoogleLogin: handleGoogleLogin, onAppleLogin: handleAppleLogin, onPhoneLogin: () => setMode('phone'), onMagicLinkLogin: () => setMode('magic-link'), onWhatsAppLogin: () => setMode('whatsapp'), loading: loading }));
|
|
15282
15621
|
}
|
|
15283
15622
|
// Button mode: show provider selection first, then email form if email is selected
|
|
15284
15623
|
if (emailDisplayMode === 'button' && !showEmailForm) {
|
|
15285
|
-
return (jsx(ProviderButtons, { enabledProviders: actualProviders, providerOrder: providerOrder, onEmailLogin: () => setShowEmailForm(true), onGoogleLogin: handleGoogleLogin, onPhoneLogin: () => setMode('phone'), onMagicLinkLogin: () => setMode('magic-link'), onWhatsAppLogin: () => setMode('whatsapp'), loading: loading }));
|
|
15624
|
+
return (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 }));
|
|
15286
15625
|
}
|
|
15287
15626
|
// Form mode or email button was clicked: show email form with other providers
|
|
15288
15627
|
return (jsxs(Fragment, { children: [emailDisplayMode === 'button' && showEmailForm && (jsx("button", { onClick: () => setShowEmailForm(false), style: {
|
|
@@ -15301,7 +15640,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
15301
15640
|
setShowResendVerification(false);
|
|
15302
15641
|
setShowRequestNewReset(false);
|
|
15303
15642
|
setError(undefined);
|
|
15304
|
-
}, 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 && (jsx(ProviderButtons, { enabledProviders: actualProviders.filter((p) => p !== 'email'), providerOrder: providerOrder, onGoogleLogin: handleGoogleLogin, onPhoneLogin: () => setMode('phone'), onMagicLinkLogin: () => setMode('magic-link'), onWhatsAppLogin: () => setMode('whatsapp'), loading: loading }))] }));
|
|
15643
|
+
}, 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 && (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 }))] }));
|
|
15305
15644
|
})()] })) })) : null }));
|
|
15306
15645
|
};
|
|
15307
15646
|
|
|
@@ -16442,5 +16781,11 @@ async function setDefaultAuthKitId(collectionId, authKitId) {
|
|
|
16442
16781
|
});
|
|
16443
16782
|
}
|
|
16444
16783
|
|
|
16445
|
-
|
|
16784
|
+
var defaultAuthKit = /*#__PURE__*/Object.freeze({
|
|
16785
|
+
__proto__: null,
|
|
16786
|
+
getDefaultAuthKitId: getDefaultAuthKitId,
|
|
16787
|
+
setDefaultAuthKitId: setDefaultAuthKitId
|
|
16788
|
+
});
|
|
16789
|
+
|
|
16790
|
+
export { AccountManagement, AuthProvider, AuthUIPreview, SmartlinksAuthUI as FirebaseAuthUI, ProtectedRoute, SchemaFieldRenderer, SmartlinksAuthUI, SmartlinksFrame, buildIframeSrc, evaluateConditions, getDefaultAuthKitId, getEditableFields, getErrorCode, getErrorStatusCode, getFriendlyErrorMessage, getRegistrationFields, isAdminFromRoles, isAuthError, isConflictError, isRateLimitError, isServerError, resolveFields, setDefaultAuthKitId, setStorageAdapter, sortFieldsByPlacement, tokenStorage, useAdminDetection, useAuth, useIframeMessages, useIframeResize };
|
|
16446
16791
|
//# sourceMappingURL=index.esm.js.map
|