@proveanything/smartlinks-auth-ui 0.5.21 → 0.6.0
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 +15 -0
- 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 +386 -34
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +386 -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
|
}
|
|
@@ -11960,6 +11984,24 @@ async function getStorage() {
|
|
|
11960
11984
|
storageInstance = await storageInitPromise;
|
|
11961
11985
|
return storageInstance;
|
|
11962
11986
|
}
|
|
11987
|
+
/**
|
|
11988
|
+
* Install a custom storage backend (e.g. Capacitor Keychain / Keystore).
|
|
11989
|
+
*
|
|
11990
|
+
* Must be called BEFORE `getStorage()` is invoked for the first time
|
|
11991
|
+
* (typically right after your app boots, before `<SmartlinksAuthUI />` mounts).
|
|
11992
|
+
* The supplied adapter then handles ALL token, user, and account persistence
|
|
11993
|
+
* — replacing the default IndexedDB → localStorage → in-memory chain.
|
|
11994
|
+
*
|
|
11995
|
+
* Intended for native shells (Capacitor / Capgo) that want tokens stored in
|
|
11996
|
+
* the OS secure enclave (Keychain on iOS, Keystore on Android) instead of the
|
|
11997
|
+
* WebView's IndexedDB. Implement the `PersistentStorage` interface against
|
|
11998
|
+
* your secure-storage plugin and pass the instance here.
|
|
11999
|
+
*/
|
|
12000
|
+
function setStorageAdapter(adapter) {
|
|
12001
|
+
StorageFactory.reset();
|
|
12002
|
+
storageInstance = adapter;
|
|
12003
|
+
storageInitPromise = Promise.resolve(adapter);
|
|
12004
|
+
}
|
|
11963
12005
|
/**
|
|
11964
12006
|
* Listen for storage changes from other tabs
|
|
11965
12007
|
*/
|
|
@@ -11978,6 +12020,7 @@ function onStorageChange(callback) {
|
|
|
11978
12020
|
}
|
|
11979
12021
|
|
|
11980
12022
|
const TOKEN_KEY = 'token';
|
|
12023
|
+
const REFRESH_TOKEN_KEY = 'refresh_token';
|
|
11981
12024
|
const USER_KEY = 'user';
|
|
11982
12025
|
const ACCOUNT_DATA_KEY = 'account_data';
|
|
11983
12026
|
const ACCOUNT_INFO_KEY = 'account_info';
|
|
@@ -12016,6 +12059,28 @@ const tokenStorage = {
|
|
|
12016
12059
|
const storage = await getStorage();
|
|
12017
12060
|
await storage.removeItem(TOKEN_KEY);
|
|
12018
12061
|
},
|
|
12062
|
+
// ----- Refresh token (native / long-lived sessions) ------------------
|
|
12063
|
+
async saveRefreshToken(token, expiresAt) {
|
|
12064
|
+
const storage = await getStorage();
|
|
12065
|
+
const payload = { token, expiresAt };
|
|
12066
|
+
await storage.setItem(REFRESH_TOKEN_KEY, payload);
|
|
12067
|
+
},
|
|
12068
|
+
async getRefreshToken() {
|
|
12069
|
+
const storage = await getStorage();
|
|
12070
|
+
const rt = await storage.getItem(REFRESH_TOKEN_KEY);
|
|
12071
|
+
if (!rt)
|
|
12072
|
+
return null;
|
|
12073
|
+
if (rt.expiresAt && rt.expiresAt < Date.now()) {
|
|
12074
|
+
console.log('[TokenStorage] Refresh token expired - clearing');
|
|
12075
|
+
await this.clearRefreshToken();
|
|
12076
|
+
return null;
|
|
12077
|
+
}
|
|
12078
|
+
return rt;
|
|
12079
|
+
},
|
|
12080
|
+
async clearRefreshToken() {
|
|
12081
|
+
const storage = await getStorage();
|
|
12082
|
+
await storage.removeItem(REFRESH_TOKEN_KEY);
|
|
12083
|
+
},
|
|
12019
12084
|
async saveUser(user) {
|
|
12020
12085
|
const storage = await getStorage();
|
|
12021
12086
|
await storage.setItem(USER_KEY, user);
|
|
@@ -12030,6 +12095,7 @@ const tokenStorage = {
|
|
|
12030
12095
|
},
|
|
12031
12096
|
async clearAll() {
|
|
12032
12097
|
await this.clearToken();
|
|
12098
|
+
await this.clearRefreshToken();
|
|
12033
12099
|
await this.clearUser();
|
|
12034
12100
|
await this.clearAccountData();
|
|
12035
12101
|
await this.clearAccountInfo();
|
|
@@ -12087,12 +12153,18 @@ const tokenStorage = {
|
|
|
12087
12153
|
|
|
12088
12154
|
// Export context for optional usage (e.g., SmartlinksFrame can work without AuthProvider)
|
|
12089
12155
|
const AuthContext = React.createContext(undefined);
|
|
12090
|
-
const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 * 1000, preloadAccountInfo = false,
|
|
12156
|
+
const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 * 1000, preloadAccountInfo = false, clientId: clientIdProp,
|
|
12091
12157
|
// Token refresh settings
|
|
12092
12158
|
enableAutoRefresh = true, refreshThresholdPercent = 75, // Refresh when 75% of token lifetime has passed
|
|
12093
12159
|
refreshCheckInterval = 60 * 1000, // Check every minute
|
|
12160
|
+
refreshOnResume,
|
|
12094
12161
|
// Contact & Interaction features
|
|
12095
12162
|
collectionId, enableContactSync, enableInteractionTracking, interactionAppId, interactionConfig, }) => {
|
|
12163
|
+
// Resolved client ID — explicit prop wins; otherwise fall back to the
|
|
12164
|
+
// collection's defaultAuthKitId (the standard pattern — see
|
|
12165
|
+
// mem://architecture/default-authkit-collection-property).
|
|
12166
|
+
const [resolvedClientId, setResolvedClientId] = React.useState(clientIdProp);
|
|
12167
|
+
const clientId = resolvedClientId;
|
|
12096
12168
|
const [user, setUser] = React.useState(null);
|
|
12097
12169
|
const [token, setToken] = React.useState(null);
|
|
12098
12170
|
const [accountData, setAccountData] = React.useState(null);
|
|
@@ -12108,8 +12180,8 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
12108
12180
|
const pendingVerificationRef = React.useRef(false);
|
|
12109
12181
|
const proxyRefreshInFlightRef = React.useRef(false);
|
|
12110
12182
|
// Stable refs for callbacks to avoid useEffect dependency cycles
|
|
12111
|
-
const syncContactRef = React.useRef();
|
|
12112
|
-
const trackInteractionRef = React.useRef();
|
|
12183
|
+
const syncContactRef = React.useRef(undefined);
|
|
12184
|
+
const trackInteractionRef = React.useRef(undefined);
|
|
12113
12185
|
// Default to enabled if collectionId is provided
|
|
12114
12186
|
const shouldSyncContacts = enableContactSync ?? !!collectionId;
|
|
12115
12187
|
const shouldTrackInteractions = enableInteractionTracking ?? !!collectionId;
|
|
@@ -12792,9 +12864,7 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
12792
12864
|
});
|
|
12793
12865
|
});
|
|
12794
12866
|
};
|
|
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
|
-
) => {
|
|
12867
|
+
const login = React.useCallback(async (authToken, authUser, authAccountData, isNewUser, expiresAt, refreshTokenValue, refreshTokenExpiresAt) => {
|
|
12798
12868
|
try {
|
|
12799
12869
|
// Only persist to storage in standalone mode
|
|
12800
12870
|
if (!proxyMode) {
|
|
@@ -12808,6 +12878,15 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
12808
12878
|
else {
|
|
12809
12879
|
await tokenStorage.clearAccountData();
|
|
12810
12880
|
}
|
|
12881
|
+
// Persist refresh token when the backend issued one (native clients).
|
|
12882
|
+
if (refreshTokenValue) {
|
|
12883
|
+
const rtExp = refreshTokenExpiresAt
|
|
12884
|
+
?? Date.now() + 90 * 24 * 60 * 60 * 1000; // 90d fallback per spec
|
|
12885
|
+
await tokenStorage.saveRefreshToken(refreshTokenValue, rtExp);
|
|
12886
|
+
}
|
|
12887
|
+
else {
|
|
12888
|
+
await tokenStorage.clearRefreshToken();
|
|
12889
|
+
}
|
|
12811
12890
|
smartlinks__namespace.auth.verifyToken(authToken).catch(() => { });
|
|
12812
12891
|
}
|
|
12813
12892
|
// Always update memory state (but NOT isVerified yet - wait for parent ack in iframe mode)
|
|
@@ -12871,6 +12950,17 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
12871
12950
|
}
|
|
12872
12951
|
// Only clear persistent storage in standalone mode
|
|
12873
12952
|
if (!proxyMode) {
|
|
12953
|
+
// Best-effort: revoke the refresh-token family server-side before
|
|
12954
|
+
// wiping local state, so a stolen copy is dead immediately.
|
|
12955
|
+
try {
|
|
12956
|
+
const storedRefresh = await tokenStorage.getRefreshToken();
|
|
12957
|
+
if (storedRefresh?.token && clientId) {
|
|
12958
|
+
await smartlinks__namespace.authKit.logout(clientId, storedRefresh.token);
|
|
12959
|
+
}
|
|
12960
|
+
}
|
|
12961
|
+
catch (err) {
|
|
12962
|
+
console.warn('[AuthContext] Refresh-token revoke failed (continuing):', err);
|
|
12963
|
+
}
|
|
12874
12964
|
await tokenStorage.clearAll();
|
|
12875
12965
|
smartlinks__namespace.auth.logout();
|
|
12876
12966
|
}
|
|
@@ -12896,7 +12986,7 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
12896
12986
|
catch (error) {
|
|
12897
12987
|
console.error('Failed to clear auth data from storage:', error);
|
|
12898
12988
|
}
|
|
12899
|
-
}, [proxyMode, notifyAuthStateChange, user, contactId, collectionId, shouldTrackInteractions, trackInteraction]);
|
|
12989
|
+
}, [proxyMode, clientId, notifyAuthStateChange, user, contactId, collectionId, shouldTrackInteractions, trackInteraction]);
|
|
12900
12990
|
const getToken = React.useCallback(async () => {
|
|
12901
12991
|
if (proxyMode) {
|
|
12902
12992
|
return token;
|
|
@@ -12923,12 +13013,44 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
12923
13013
|
expiresIn: Math.max(0, storedToken.expiresAt - Date.now()),
|
|
12924
13014
|
};
|
|
12925
13015
|
}, [proxyMode, token]);
|
|
12926
|
-
// Refresh token -
|
|
13016
|
+
// Refresh token - prefers the AuthKit refresh-token endpoint when a refresh
|
|
13017
|
+
// token is stored (native clients); otherwise falls back to verify-and-extend
|
|
13018
|
+
// for web clients whose backend still issues only short-lived JWTs.
|
|
12927
13019
|
const refreshToken = React.useCallback(async () => {
|
|
12928
13020
|
if (proxyMode) {
|
|
12929
13021
|
throw new Error('Token refresh in proxy mode is handled by the parent application');
|
|
12930
13022
|
}
|
|
12931
13023
|
const storedToken = await tokenStorage.getToken();
|
|
13024
|
+
const storedRefresh = await tokenStorage.getRefreshToken();
|
|
13025
|
+
// -------- Path A: true refresh-token rotation (native) ---------------
|
|
13026
|
+
if (storedRefresh?.token && clientId) {
|
|
13027
|
+
try {
|
|
13028
|
+
const resp = await smartlinks__namespace.authKit.refreshToken(clientId, storedRefresh.token);
|
|
13029
|
+
await tokenStorage.saveToken(resp.token, resp.expiresAt);
|
|
13030
|
+
await tokenStorage.saveRefreshToken(resp.refreshToken, resp.refreshTokenExpiresAt);
|
|
13031
|
+
setToken(resp.token);
|
|
13032
|
+
setIsVerified(true);
|
|
13033
|
+
pendingVerificationRef.current = false;
|
|
13034
|
+
notifyAuthStateChange('TOKEN_REFRESH', user, resp.token, accountData, accountInfo, true, contact, contactId);
|
|
13035
|
+
return resp.token;
|
|
13036
|
+
}
|
|
13037
|
+
catch (error) {
|
|
13038
|
+
console.error('[AuthContext] Refresh-token rotation failed:', error);
|
|
13039
|
+
if (isNetworkError(error))
|
|
13040
|
+
throw error;
|
|
13041
|
+
// Reuse / invalid / expired → force re-login. The SDK surfaces these
|
|
13042
|
+
// as RefreshErrorCode (INVALID_REFRESH_TOKEN / REUSE_DETECTED / MISSING).
|
|
13043
|
+
const code = error?.errorCode ?? error?.code;
|
|
13044
|
+
if (code === 'INVALID_REFRESH_TOKEN' ||
|
|
13045
|
+
code === 'REFRESH_TOKEN_REUSE_DETECTED' ||
|
|
13046
|
+
code === 'MISSING_REFRESH_TOKEN') {
|
|
13047
|
+
await logout();
|
|
13048
|
+
throw new Error('Session expired. Please login again.');
|
|
13049
|
+
}
|
|
13050
|
+
// Fall through to verify-and-extend on unexpected errors.
|
|
13051
|
+
}
|
|
13052
|
+
}
|
|
13053
|
+
// -------- Path B: legacy verify-and-extend (web) ---------------------
|
|
12932
13054
|
if (!storedToken?.token) {
|
|
12933
13055
|
throw new Error('No token to refresh. Please login first.');
|
|
12934
13056
|
}
|
|
@@ -12953,7 +13075,7 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
12953
13075
|
await logout();
|
|
12954
13076
|
throw new Error('Token refresh failed. Please login again.');
|
|
12955
13077
|
}
|
|
12956
|
-
}, [proxyMode, user, accountData, accountInfo, contact, contactId, notifyAuthStateChange, isNetworkError, logout]);
|
|
13078
|
+
}, [proxyMode, clientId, user, accountData, accountInfo, contact, contactId, notifyAuthStateChange, isNetworkError, logout]);
|
|
12957
13079
|
const getAccount = React.useCallback(async (forceRefresh = false) => {
|
|
12958
13080
|
try {
|
|
12959
13081
|
if (proxyMode) {
|
|
@@ -13117,6 +13239,105 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
13117
13239
|
clearInterval(intervalId);
|
|
13118
13240
|
};
|
|
13119
13241
|
}, [proxyMode, enableAutoRefresh, refreshCheckInterval, refreshThresholdPercent, token, user, refreshToken]);
|
|
13242
|
+
// Resolve clientId from the collection's defaultAuthKitId when not provided
|
|
13243
|
+
// explicitly. Most apps only pass `collectionId` — this means refresh-token
|
|
13244
|
+
// rotation and server-side logout revocation still work end-to-end.
|
|
13245
|
+
React.useEffect(() => {
|
|
13246
|
+
if (clientIdProp) {
|
|
13247
|
+
setResolvedClientId(clientIdProp);
|
|
13248
|
+
return;
|
|
13249
|
+
}
|
|
13250
|
+
if (!collectionId)
|
|
13251
|
+
return;
|
|
13252
|
+
let cancelled = false;
|
|
13253
|
+
(async () => {
|
|
13254
|
+
try {
|
|
13255
|
+
const { getDefaultAuthKitId } = await Promise.resolve().then(function () { return defaultAuthKit; });
|
|
13256
|
+
const id = await getDefaultAuthKitId(collectionId);
|
|
13257
|
+
if (!cancelled && id)
|
|
13258
|
+
setResolvedClientId(id);
|
|
13259
|
+
}
|
|
13260
|
+
catch (err) {
|
|
13261
|
+
console.warn('[AuthContext] Could not resolve default authKitId from collection:', err);
|
|
13262
|
+
}
|
|
13263
|
+
})();
|
|
13264
|
+
return () => { cancelled = true; };
|
|
13265
|
+
}, [clientIdProp, collectionId]);
|
|
13266
|
+
// Refresh-on-resume: fires when the app returns to the foreground.
|
|
13267
|
+
// - Capacitor: `App.addListener('appStateChange')` via dynamic import (no
|
|
13268
|
+
// runtime dep on web).
|
|
13269
|
+
// - Universal fallback: `document.visibilitychange` — also catches PWAs /
|
|
13270
|
+
// web tabs that have been backgrounded for hours.
|
|
13271
|
+
React.useEffect(() => {
|
|
13272
|
+
if (proxyMode || !enableAutoRefresh || !token || !user)
|
|
13273
|
+
return;
|
|
13274
|
+
// Default ON when we detect a native shell; OFF otherwise.
|
|
13275
|
+
const isNative = (() => {
|
|
13276
|
+
try {
|
|
13277
|
+
const cap = window.Capacitor;
|
|
13278
|
+
if (cap?.isNativePlatform?.() === true)
|
|
13279
|
+
return true;
|
|
13280
|
+
if (window.AuthKit?.isNative === true)
|
|
13281
|
+
return true;
|
|
13282
|
+
}
|
|
13283
|
+
catch { /* noop */ }
|
|
13284
|
+
return false;
|
|
13285
|
+
})();
|
|
13286
|
+
const enabled = refreshOnResume ?? isNative;
|
|
13287
|
+
if (!enabled)
|
|
13288
|
+
return;
|
|
13289
|
+
const maybeRefresh = async () => {
|
|
13290
|
+
try {
|
|
13291
|
+
const storedToken = await tokenStorage.getToken();
|
|
13292
|
+
if (!storedToken?.expiresAt)
|
|
13293
|
+
return;
|
|
13294
|
+
const remainingMs = storedToken.expiresAt - Date.now();
|
|
13295
|
+
const totalLifetimeMs = 7 * 24 * 60 * 60 * 1000;
|
|
13296
|
+
const percentUsed = ((totalLifetimeMs - remainingMs) / totalLifetimeMs) * 100;
|
|
13297
|
+
if (percentUsed < refreshThresholdPercent)
|
|
13298
|
+
return;
|
|
13299
|
+
await refreshToken().catch(() => { });
|
|
13300
|
+
}
|
|
13301
|
+
catch (err) {
|
|
13302
|
+
console.error('[AuthContext] Resume refresh check failed:', err);
|
|
13303
|
+
}
|
|
13304
|
+
};
|
|
13305
|
+
// Universal visibility listener
|
|
13306
|
+
const onVisibility = () => {
|
|
13307
|
+
if (document.visibilityState === 'visible')
|
|
13308
|
+
maybeRefresh();
|
|
13309
|
+
};
|
|
13310
|
+
document.addEventListener('visibilitychange', onVisibility);
|
|
13311
|
+
// Capacitor listener (optional)
|
|
13312
|
+
let capRemove;
|
|
13313
|
+
(async () => {
|
|
13314
|
+
try {
|
|
13315
|
+
// Only attempt to load @capacitor/app in an actual native shell.
|
|
13316
|
+
// The specifier is obfuscated so bundlers (Vite/webpack) don't try
|
|
13317
|
+
// to statically resolve an optional peer dependency at build time.
|
|
13318
|
+
const isNative = typeof window !== 'undefined' &&
|
|
13319
|
+
(window.Capacitor?.isNativePlatform?.() === true ||
|
|
13320
|
+
window.AuthKit?.isNative === true);
|
|
13321
|
+
if (!isNative)
|
|
13322
|
+
return;
|
|
13323
|
+
const specifier = ['@capacitor', 'app'].join('/');
|
|
13324
|
+
const dynamicImport = new Function('s', 'return import(s)');
|
|
13325
|
+
const mod = await dynamicImport(specifier);
|
|
13326
|
+
const handle = await mod.App.addListener('appStateChange', (state) => {
|
|
13327
|
+
if (state.isActive)
|
|
13328
|
+
maybeRefresh();
|
|
13329
|
+
});
|
|
13330
|
+
capRemove = () => handle?.remove?.();
|
|
13331
|
+
}
|
|
13332
|
+
catch {
|
|
13333
|
+
// @capacitor/app not installed or unavailable — visibilitychange handles it.
|
|
13334
|
+
}
|
|
13335
|
+
})();
|
|
13336
|
+
return () => {
|
|
13337
|
+
document.removeEventListener('visibilitychange', onVisibility);
|
|
13338
|
+
capRemove?.();
|
|
13339
|
+
};
|
|
13340
|
+
}, [proxyMode, enableAutoRefresh, refreshOnResume, refreshThresholdPercent, token, user, refreshToken]);
|
|
13120
13341
|
const value = {
|
|
13121
13342
|
user,
|
|
13122
13343
|
token,
|
|
@@ -13378,8 +13599,22 @@ const loadGoogleIdentityServices = () => {
|
|
|
13378
13599
|
document.head.appendChild(script);
|
|
13379
13600
|
});
|
|
13380
13601
|
};
|
|
13602
|
+
// Helper to detect a Capacitor native runtime (iOS/Android shell, not the web)
|
|
13603
|
+
const isCapacitorNative = () => {
|
|
13604
|
+
const cap = window.Capacitor;
|
|
13605
|
+
if (!cap)
|
|
13606
|
+
return false;
|
|
13607
|
+
// isNativePlatform() is the modern API; fall back to platform string for older cores
|
|
13608
|
+
if (typeof cap.isNativePlatform === 'function')
|
|
13609
|
+
return cap.isNativePlatform();
|
|
13610
|
+
return cap.platform === 'ios' || cap.platform === 'android';
|
|
13611
|
+
};
|
|
13381
13612
|
// Helper to detect WebView environments (Android/iOS)
|
|
13382
13613
|
const detectWebView = () => {
|
|
13614
|
+
// Capacitor apps run inside a WebView (WKWebView/Android WebView) but don't
|
|
13615
|
+
// always set the `wv` UA token — treat them as WebView explicitly.
|
|
13616
|
+
if (isCapacitorNative())
|
|
13617
|
+
return true;
|
|
13383
13618
|
const ua = navigator.userAgent;
|
|
13384
13619
|
// Android WebView detection
|
|
13385
13620
|
if (/Android/i.test(ua)) {
|
|
@@ -13487,7 +13722,7 @@ const checkSilentGoogleSignIn = async (clientId, googleClientId) => {
|
|
|
13487
13722
|
});
|
|
13488
13723
|
};
|
|
13489
13724
|
// 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, }) => {
|
|
13725
|
+
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
13726
|
// Resolve signup prominence from props, customization, config, or default
|
|
13492
13727
|
const resolvedSignupProminence = signupProminence || customization?.signupProminence || 'minimal';
|
|
13493
13728
|
// Determine initial mode based on signupProminence setting
|
|
@@ -13560,17 +13795,48 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13560
13795
|
}
|
|
13561
13796
|
};
|
|
13562
13797
|
// Dark mode detection and theme management
|
|
13798
|
+
// In 'auto' mode we honor (in priority order):
|
|
13799
|
+
// 1. An explicit `.dark` class on <html> or <body> (set by the host portal / SmartLinks theme system)
|
|
13800
|
+
// 2. A SmartLinks theme payload in the URL (?theme=... with m === 'd')
|
|
13801
|
+
// 3. The OS `prefers-color-scheme` preference
|
|
13563
13802
|
React.useEffect(() => {
|
|
13564
13803
|
if (theme !== 'auto') {
|
|
13565
13804
|
setResolvedTheme(theme);
|
|
13566
13805
|
return;
|
|
13567
13806
|
}
|
|
13568
|
-
// Auto-detect system theme preference
|
|
13569
13807
|
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
13570
|
-
const
|
|
13808
|
+
const hostHasDarkClass = () => document.documentElement.classList.contains('dark') ||
|
|
13809
|
+
document.body.classList.contains('dark');
|
|
13810
|
+
const urlRequestsDark = () => {
|
|
13811
|
+
try {
|
|
13812
|
+
const search = new URLSearchParams(window.location.search);
|
|
13813
|
+
let themeParam = search.get('theme');
|
|
13814
|
+
if (!themeParam && window.location.hash.includes('?')) {
|
|
13815
|
+
themeParam = new URLSearchParams(window.location.hash.substring(window.location.hash.indexOf('?'))).get('theme');
|
|
13816
|
+
}
|
|
13817
|
+
if (!themeParam)
|
|
13818
|
+
return false;
|
|
13819
|
+
const decoded = JSON.parse(atob(themeParam));
|
|
13820
|
+
return decoded?.m === 'd';
|
|
13821
|
+
}
|
|
13822
|
+
catch {
|
|
13823
|
+
return false;
|
|
13824
|
+
}
|
|
13825
|
+
};
|
|
13826
|
+
const updateTheme = () => {
|
|
13827
|
+
const isDark = hostHasDarkClass() || urlRequestsDark() || mediaQuery.matches;
|
|
13828
|
+
setResolvedTheme(isDark ? 'dark' : 'light');
|
|
13829
|
+
};
|
|
13571
13830
|
updateTheme();
|
|
13572
13831
|
mediaQuery.addEventListener('change', updateTheme);
|
|
13573
|
-
|
|
13832
|
+
// Watch host <html>/<body> class changes (portal toggling dark mode at runtime)
|
|
13833
|
+
const observer = new MutationObserver(updateTheme);
|
|
13834
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
|
13835
|
+
observer.observe(document.body, { attributes: true, attributeFilter: ['class'] });
|
|
13836
|
+
return () => {
|
|
13837
|
+
mediaQuery.removeEventListener('change', updateTheme);
|
|
13838
|
+
observer.disconnect();
|
|
13839
|
+
};
|
|
13574
13840
|
}, [theme]);
|
|
13575
13841
|
// Version tracking for debugging if needed
|
|
13576
13842
|
// console.log(`${LOG_PREFIX} Component mounted, v${AUTH_UI_VERSION}`);
|
|
@@ -13756,23 +14022,28 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13756
14022
|
};
|
|
13757
14023
|
fetchSchema();
|
|
13758
14024
|
}, [collectionId, sdkReady]);
|
|
13759
|
-
// Silent
|
|
13760
|
-
//
|
|
14025
|
+
// Silent Sign-In check on mount (for native apps).
|
|
14026
|
+
// Prefers the injected `nativeAuth.checkSignIn` adapter (Capacitor) when
|
|
14027
|
+
// `enableSilentNativeSignIn` is set; otherwise falls back to the legacy
|
|
14028
|
+
// `window.AuthKit` bridge when `enableSilentGoogleSignIn` is set.
|
|
14029
|
+
const wantsSilentNative = enableSilentNativeSignIn && !!nativeAuth?.checkSignIn;
|
|
13761
14030
|
React.useEffect(() => {
|
|
13762
|
-
if (!enableSilentGoogleSignIn || silentSignInChecked || !sdkReady || auth.isAuthenticated) {
|
|
14031
|
+
if ((!enableSilentGoogleSignIn && !wantsSilentNative) || silentSignInChecked || !sdkReady || auth.isAuthenticated) {
|
|
13763
14032
|
return;
|
|
13764
14033
|
}
|
|
13765
14034
|
const googleClientId = config?.googleClientId || DEFAULT_GOOGLE_CLIENT_ID;
|
|
13766
14035
|
const performSilentSignIn = async () => {
|
|
13767
14036
|
try {
|
|
13768
|
-
const result =
|
|
14037
|
+
const result = wantsSilentNative
|
|
14038
|
+
? await nativeAuth.checkSignIn({ serverClientId: googleClientId })
|
|
14039
|
+
: await checkSilentGoogleSignIn(clientId, googleClientId);
|
|
13769
14040
|
setSilentSignInChecked(true);
|
|
13770
14041
|
if (result?.isSignedIn && result.idToken) {
|
|
13771
14042
|
setLoading(true);
|
|
13772
14043
|
try {
|
|
13773
14044
|
const authResponse = await api.loginWithGoogle(result.idToken);
|
|
13774
14045
|
if (authResponse.token) {
|
|
13775
|
-
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, false, getExpirationFromResponse(authResponse));
|
|
14046
|
+
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, false, getExpirationFromResponse(authResponse), authResponse.refreshToken, authResponse.refreshTokenExpiresAt);
|
|
13776
14047
|
setAuthSuccess(true);
|
|
13777
14048
|
setSuccessMessage('Signed in automatically with Google!');
|
|
13778
14049
|
onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
|
|
@@ -13791,7 +14062,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13791
14062
|
}
|
|
13792
14063
|
};
|
|
13793
14064
|
performSilentSignIn();
|
|
13794
|
-
}, [enableSilentGoogleSignIn, silentSignInChecked, sdkReady, auth.isAuthenticated, clientId, config?.googleClientId, api, auth, onAuthSuccess]);
|
|
14065
|
+
}, [enableSilentGoogleSignIn, wantsSilentNative, nativeAuth, silentSignInChecked, sdkReady, auth.isAuthenticated, clientId, config?.googleClientId, api, auth, onAuthSuccess]);
|
|
13795
14066
|
// Reset showEmailForm when mode changes away from login/register
|
|
13796
14067
|
React.useEffect(() => {
|
|
13797
14068
|
if (mode !== 'login' && mode !== 'register') {
|
|
@@ -13867,7 +14138,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13867
14138
|
if ((verificationMode === 'verify-auto-login' || verificationMode === 'immediate') && response.token) {
|
|
13868
14139
|
// Auto-login modes: Log the user in immediately if token is provided
|
|
13869
14140
|
// Always await - auth.login now waits for parent ack automatically in iframe mode
|
|
13870
|
-
await auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response));
|
|
14141
|
+
await auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response), response.refreshToken, response.refreshTokenExpiresAt);
|
|
13871
14142
|
setUrlAuthProcessing(false); // Clear processing state before redirect/success
|
|
13872
14143
|
if (redirectUrl) {
|
|
13873
14144
|
// Redirect to clean URL and resume flow
|
|
@@ -13927,7 +14198,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13927
14198
|
// Auto-login with magic link if token is provided
|
|
13928
14199
|
if (response.token) {
|
|
13929
14200
|
// Always await - auth.login now waits for parent ack automatically in iframe mode
|
|
13930
|
-
await auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response));
|
|
14201
|
+
await auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response), response.refreshToken, response.refreshTokenExpiresAt);
|
|
13931
14202
|
setUrlAuthProcessing(false); // Clear processing state before redirect/success
|
|
13932
14203
|
if (redirectUrl) {
|
|
13933
14204
|
// Redirect to clean URL and resume flow
|
|
@@ -14002,7 +14273,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14002
14273
|
log.log('Google OAuth code exchange response:', { hasToken: !!response.token, hasUser: !!response.user, isNewUser: response.isNewUser });
|
|
14003
14274
|
if (response.token) {
|
|
14004
14275
|
// Await login to ensure token is persisted before any navigation
|
|
14005
|
-
await auth.login(response.token, response.user, response.accountData, response.isNewUser, getExpirationFromResponse(response));
|
|
14276
|
+
await auth.login(response.token, response.user, response.accountData, response.isNewUser, getExpirationFromResponse(response), response.refreshToken, response.refreshTokenExpiresAt);
|
|
14006
14277
|
setAuthSuccess(true);
|
|
14007
14278
|
setSuccessMessage('Google login successful!');
|
|
14008
14279
|
onAuthSuccess(response.token, response.user, response.accountData);
|
|
@@ -14056,7 +14327,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14056
14327
|
// Handle different verification modes
|
|
14057
14328
|
if (verificationMode === 'immediate' && response.token) {
|
|
14058
14329
|
// Immediate mode: Log in right away if token is provided (isNewUser=true for registration)
|
|
14059
|
-
await auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response));
|
|
14330
|
+
await auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response), response.refreshToken, response.refreshTokenExpiresAt);
|
|
14060
14331
|
setAuthSuccess(true);
|
|
14061
14332
|
const deadline = response.emailVerificationDeadline
|
|
14062
14333
|
? new Date(response.emailVerificationDeadline).toLocaleString()
|
|
@@ -14118,7 +14389,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14118
14389
|
setLoading(false);
|
|
14119
14390
|
return;
|
|
14120
14391
|
}
|
|
14121
|
-
await auth.login(response.token, response.user, response.accountData, false, getExpirationFromResponse(response));
|
|
14392
|
+
await auth.login(response.token, response.user, response.accountData, false, getExpirationFromResponse(response), response.refreshToken, response.refreshTokenExpiresAt);
|
|
14122
14393
|
setAuthSuccess(true);
|
|
14123
14394
|
setSuccessMessage('Login successful!');
|
|
14124
14395
|
onAuthSuccess(response.token, response.user, response.accountData);
|
|
@@ -14248,6 +14519,55 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14248
14519
|
}
|
|
14249
14520
|
});
|
|
14250
14521
|
};
|
|
14522
|
+
// Finish a login once a backend AuthResponse has been obtained (shared by the
|
|
14523
|
+
// native adapter paths for Google and Apple).
|
|
14524
|
+
const completeNativeLogin = async (authResponse, successLabel) => {
|
|
14525
|
+
if (!authResponse.token) {
|
|
14526
|
+
throw new Error('Authentication failed - no token received');
|
|
14527
|
+
}
|
|
14528
|
+
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse));
|
|
14529
|
+
setAuthSuccess(true);
|
|
14530
|
+
setSuccessMessage(successLabel);
|
|
14531
|
+
onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
|
|
14532
|
+
};
|
|
14533
|
+
// Sign in with Apple. Apple is native-only in this kit today: it requires an
|
|
14534
|
+
// injected `nativeAuth` adapter (e.g. a Capacitor plugin) that resolves an
|
|
14535
|
+
// Apple identity token. There is no in-browser fallback yet.
|
|
14536
|
+
const handleAppleLogin = async () => {
|
|
14537
|
+
if (!nativeAuth?.signInWithApple) {
|
|
14538
|
+
log.warn('Apple sign-in requested but no nativeAuth.signInWithApple adapter is configured');
|
|
14539
|
+
setError('Apple Sign-In is not available in this environment.');
|
|
14540
|
+
onAuthError?.(new Error('No nativeAuth.signInWithApple adapter configured'));
|
|
14541
|
+
return;
|
|
14542
|
+
}
|
|
14543
|
+
setLoading(true);
|
|
14544
|
+
setError(undefined);
|
|
14545
|
+
try {
|
|
14546
|
+
log.log('Using native adapter for Apple Sign-In');
|
|
14547
|
+
const result = await nativeAuth.signInWithApple({
|
|
14548
|
+
clientId: config?.appleClientId,
|
|
14549
|
+
scopes: ['name', 'email'],
|
|
14550
|
+
});
|
|
14551
|
+
if (!result?.idToken) {
|
|
14552
|
+
throw new Error('Apple Sign-In did not return an identity token');
|
|
14553
|
+
}
|
|
14554
|
+
const authResponse = await api.loginWithApple(result.idToken, {
|
|
14555
|
+
authorizationCode: result.authorizationCode,
|
|
14556
|
+
nonce: result.nonce,
|
|
14557
|
+
appleUserInfo: result.email || result.name ? { email: result.email, name: result.name } : undefined,
|
|
14558
|
+
});
|
|
14559
|
+
log.log('Native Apple login response:', { hasToken: !!authResponse.token, isNewUser: authResponse.isNewUser });
|
|
14560
|
+
await completeNativeLogin(authResponse, 'Apple login successful!');
|
|
14561
|
+
}
|
|
14562
|
+
catch (err) {
|
|
14563
|
+
log.error('Apple Sign-In failed:', err);
|
|
14564
|
+
setError(getFriendlyErrorMessage(err));
|
|
14565
|
+
onAuthError?.(err instanceof Error ? err : new Error(getFriendlyErrorMessage(err)));
|
|
14566
|
+
}
|
|
14567
|
+
finally {
|
|
14568
|
+
setLoading(false);
|
|
14569
|
+
}
|
|
14570
|
+
};
|
|
14251
14571
|
const handleGoogleLogin = async () => {
|
|
14252
14572
|
const hasCustomGoogleClientId = !!config?.googleClientId;
|
|
14253
14573
|
const googleClientId = config?.googleClientId || DEFAULT_GOOGLE_CLIENT_ID;
|
|
@@ -14277,6 +14597,32 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14277
14597
|
setLoading(true);
|
|
14278
14598
|
setError(undefined);
|
|
14279
14599
|
try {
|
|
14600
|
+
// Priority 0: injected Promise-based native adapter (Capacitor, etc.).
|
|
14601
|
+
// This is the clean path — await the plugin directly, no callback bridge.
|
|
14602
|
+
if (nativeAuth?.signInWithGoogle) {
|
|
14603
|
+
log.log('Using injected nativeAuth adapter for Google Sign-In');
|
|
14604
|
+
try {
|
|
14605
|
+
const result = await nativeAuth.signInWithGoogle({
|
|
14606
|
+
serverClientId: googleClientId,
|
|
14607
|
+
scopes: ['email', 'profile'],
|
|
14608
|
+
});
|
|
14609
|
+
if (!result?.idToken) {
|
|
14610
|
+
throw new Error('Google Sign-In did not return an ID token');
|
|
14611
|
+
}
|
|
14612
|
+
const authResponse = await api.loginWithGoogle(result.idToken);
|
|
14613
|
+
log.log('Native (adapter) Google login response:', { hasToken: !!authResponse.token, isNewUser: authResponse.isNewUser });
|
|
14614
|
+
await completeNativeLogin(authResponse, 'Google login successful!');
|
|
14615
|
+
}
|
|
14616
|
+
catch (err) {
|
|
14617
|
+
log.error('Adapter Google Sign-In failed:', err);
|
|
14618
|
+
setError(getFriendlyErrorMessage(err));
|
|
14619
|
+
onAuthError?.(err instanceof Error ? err : new Error(getFriendlyErrorMessage(err)));
|
|
14620
|
+
}
|
|
14621
|
+
finally {
|
|
14622
|
+
setLoading(false);
|
|
14623
|
+
}
|
|
14624
|
+
return;
|
|
14625
|
+
}
|
|
14280
14626
|
if (nativeBridge) {
|
|
14281
14627
|
log.log('Using native bridge for Google Sign-In');
|
|
14282
14628
|
const callbackId = `google_auth_${Date.now()}`;
|
|
@@ -14295,7 +14641,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14295
14641
|
const authResponse = await api.loginWithGoogle(result.idToken);
|
|
14296
14642
|
log.log('Native Google login response:', { hasToken: !!authResponse.token, hasUser: !!authResponse.user, isNewUser: authResponse.isNewUser });
|
|
14297
14643
|
if (authResponse.token) {
|
|
14298
|
-
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse));
|
|
14644
|
+
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse), authResponse.refreshToken, authResponse.refreshTokenExpiresAt);
|
|
14299
14645
|
setAuthSuccess(true);
|
|
14300
14646
|
setSuccessMessage('Google login successful!');
|
|
14301
14647
|
onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
|
|
@@ -14515,7 +14861,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14515
14861
|
log.log('Popup Google login response:', { hasToken: !!authResponse.token, hasUser: !!authResponse.user, isNewUser: authResponse.isNewUser });
|
|
14516
14862
|
if (authResponse.token) {
|
|
14517
14863
|
// Google OAuth can be login or signup - use isNewUser flag from backend if available
|
|
14518
|
-
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse));
|
|
14864
|
+
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse), authResponse.refreshToken, authResponse.refreshTokenExpiresAt);
|
|
14519
14865
|
setAuthSuccess(true);
|
|
14520
14866
|
setSuccessMessage('Google login successful!');
|
|
14521
14867
|
onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
|
|
@@ -14565,7 +14911,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14565
14911
|
log.log('OneTap Google login response:', { hasToken: !!authResponse.token, hasUser: !!authResponse.user, isNewUser: authResponse.isNewUser });
|
|
14566
14912
|
if (authResponse.token) {
|
|
14567
14913
|
// Google OAuth can be login or signup - use isNewUser flag from backend if available
|
|
14568
|
-
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse));
|
|
14914
|
+
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse), authResponse.refreshToken, authResponse.refreshTokenExpiresAt);
|
|
14569
14915
|
setAuthSuccess(true);
|
|
14570
14916
|
setSuccessMessage('Google login successful!');
|
|
14571
14917
|
onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
|
|
@@ -14647,7 +14993,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14647
14993
|
// Phone auth is an INTERACTIVE flow (user entered OTP in UI)
|
|
14648
14994
|
// Unlike deep-link flows (email verification, magic link), there's no URL token to clean up
|
|
14649
14995
|
// Do NOT auto-redirect - let the parent app control the next step (profile completion, etc.)
|
|
14650
|
-
await auth.login(response.token, response.user, response.accountData, response.isNewUser, getExpirationFromResponse(response));
|
|
14996
|
+
await auth.login(response.token, response.user, response.accountData, response.isNewUser, getExpirationFromResponse(response), response.refreshToken, response.refreshTokenExpiresAt);
|
|
14651
14997
|
setAuthSuccess(true);
|
|
14652
14998
|
setSuccessMessage('Phone verified! You are now logged in.');
|
|
14653
14999
|
onAuthSuccess(response.token, response.user, response.accountData);
|
|
@@ -14937,7 +15283,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14937
15283
|
throw Object.assign(new Error(resultErrorMessage), session);
|
|
14938
15284
|
}
|
|
14939
15285
|
if (session?.token && session.user) {
|
|
14940
|
-
await auth.login(session.token, session.user, session.accountData, true, getExpirationFromResponse(session));
|
|
15286
|
+
await auth.login(session.token, session.user, session.accountData, true, getExpirationFromResponse(session), session.refreshToken, session.refreshTokenExpiresAt);
|
|
14941
15287
|
if (!proxyMode) {
|
|
14942
15288
|
onAuthSuccess(session.token, session.user, session.accountData);
|
|
14943
15289
|
}
|
|
@@ -15267,11 +15613,11 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
15267
15613
|
const hasEmailProvider = actualProviders.includes('email');
|
|
15268
15614
|
// If email provider is not enabled, only show provider buttons (no email/password form)
|
|
15269
15615
|
if (!hasEmailProvider) {
|
|
15270
|
-
return (jsxRuntime.jsx(ProviderButtons, { enabledProviders: actualProviders, providerOrder: providerOrder, onGoogleLogin: handleGoogleLogin, onPhoneLogin: () => setMode('phone'), onMagicLinkLogin: () => setMode('magic-link'), onWhatsAppLogin: () => setMode('whatsapp'), loading: loading }));
|
|
15616
|
+
return (jsxRuntime.jsx(ProviderButtons, { enabledProviders: actualProviders, providerOrder: providerOrder, onGoogleLogin: handleGoogleLogin, onAppleLogin: handleAppleLogin, onPhoneLogin: () => setMode('phone'), onMagicLinkLogin: () => setMode('magic-link'), onWhatsAppLogin: () => setMode('whatsapp'), loading: loading }));
|
|
15271
15617
|
}
|
|
15272
15618
|
// Button mode: show provider selection first, then email form if email is selected
|
|
15273
15619
|
if (emailDisplayMode === 'button' && !showEmailForm) {
|
|
15274
|
-
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 }));
|
|
15620
|
+
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 }));
|
|
15275
15621
|
}
|
|
15276
15622
|
// Form mode or email button was clicked: show email form with other providers
|
|
15277
15623
|
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [emailDisplayMode === 'button' && showEmailForm && (jsxRuntime.jsx("button", { onClick: () => setShowEmailForm(false), style: {
|
|
@@ -15290,7 +15636,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
15290
15636
|
setShowResendVerification(false);
|
|
15291
15637
|
setShowRequestNewReset(false);
|
|
15292
15638
|
setError(undefined);
|
|
15293
|
-
}, 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 }))] }));
|
|
15639
|
+
}, 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 }))] }));
|
|
15294
15640
|
})()] })) })) : null }));
|
|
15295
15641
|
};
|
|
15296
15642
|
|
|
@@ -16431,6 +16777,12 @@ async function setDefaultAuthKitId(collectionId, authKitId) {
|
|
|
16431
16777
|
});
|
|
16432
16778
|
}
|
|
16433
16779
|
|
|
16780
|
+
var defaultAuthKit = /*#__PURE__*/Object.freeze({
|
|
16781
|
+
__proto__: null,
|
|
16782
|
+
getDefaultAuthKitId: getDefaultAuthKitId,
|
|
16783
|
+
setDefaultAuthKitId: setDefaultAuthKitId
|
|
16784
|
+
});
|
|
16785
|
+
|
|
16434
16786
|
exports.AccountManagement = AccountManagement;
|
|
16435
16787
|
exports.AuthProvider = AuthProvider;
|
|
16436
16788
|
exports.AuthUIPreview = AuthUIPreview;
|
|
@@ -16454,6 +16806,7 @@ exports.isRateLimitError = isRateLimitError;
|
|
|
16454
16806
|
exports.isServerError = isServerError;
|
|
16455
16807
|
exports.resolveFields = resolveFields;
|
|
16456
16808
|
exports.setDefaultAuthKitId = setDefaultAuthKitId;
|
|
16809
|
+
exports.setStorageAdapter = setStorageAdapter;
|
|
16457
16810
|
exports.sortFieldsByPlacement = sortFieldsByPlacement;
|
|
16458
16811
|
exports.tokenStorage = tokenStorage;
|
|
16459
16812
|
exports.useAdminDetection = useAdminDetection;
|