@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.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
|
}
|
|
@@ -11940,6 +11964,24 @@ async function getStorage() {
|
|
|
11940
11964
|
storageInstance = await storageInitPromise;
|
|
11941
11965
|
return storageInstance;
|
|
11942
11966
|
}
|
|
11967
|
+
/**
|
|
11968
|
+
* Install a custom storage backend (e.g. Capacitor Keychain / Keystore).
|
|
11969
|
+
*
|
|
11970
|
+
* Must be called BEFORE `getStorage()` is invoked for the first time
|
|
11971
|
+
* (typically right after your app boots, before `<SmartlinksAuthUI />` mounts).
|
|
11972
|
+
* The supplied adapter then handles ALL token, user, and account persistence
|
|
11973
|
+
* — replacing the default IndexedDB → localStorage → in-memory chain.
|
|
11974
|
+
*
|
|
11975
|
+
* Intended for native shells (Capacitor / Capgo) that want tokens stored in
|
|
11976
|
+
* the OS secure enclave (Keychain on iOS, Keystore on Android) instead of the
|
|
11977
|
+
* WebView's IndexedDB. Implement the `PersistentStorage` interface against
|
|
11978
|
+
* your secure-storage plugin and pass the instance here.
|
|
11979
|
+
*/
|
|
11980
|
+
function setStorageAdapter(adapter) {
|
|
11981
|
+
StorageFactory.reset();
|
|
11982
|
+
storageInstance = adapter;
|
|
11983
|
+
storageInitPromise = Promise.resolve(adapter);
|
|
11984
|
+
}
|
|
11943
11985
|
/**
|
|
11944
11986
|
* Listen for storage changes from other tabs
|
|
11945
11987
|
*/
|
|
@@ -11958,6 +12000,7 @@ function onStorageChange(callback) {
|
|
|
11958
12000
|
}
|
|
11959
12001
|
|
|
11960
12002
|
const TOKEN_KEY = 'token';
|
|
12003
|
+
const REFRESH_TOKEN_KEY = 'refresh_token';
|
|
11961
12004
|
const USER_KEY = 'user';
|
|
11962
12005
|
const ACCOUNT_DATA_KEY = 'account_data';
|
|
11963
12006
|
const ACCOUNT_INFO_KEY = 'account_info';
|
|
@@ -11996,6 +12039,28 @@ const tokenStorage = {
|
|
|
11996
12039
|
const storage = await getStorage();
|
|
11997
12040
|
await storage.removeItem(TOKEN_KEY);
|
|
11998
12041
|
},
|
|
12042
|
+
// ----- Refresh token (native / long-lived sessions) ------------------
|
|
12043
|
+
async saveRefreshToken(token, expiresAt) {
|
|
12044
|
+
const storage = await getStorage();
|
|
12045
|
+
const payload = { token, expiresAt };
|
|
12046
|
+
await storage.setItem(REFRESH_TOKEN_KEY, payload);
|
|
12047
|
+
},
|
|
12048
|
+
async getRefreshToken() {
|
|
12049
|
+
const storage = await getStorage();
|
|
12050
|
+
const rt = await storage.getItem(REFRESH_TOKEN_KEY);
|
|
12051
|
+
if (!rt)
|
|
12052
|
+
return null;
|
|
12053
|
+
if (rt.expiresAt && rt.expiresAt < Date.now()) {
|
|
12054
|
+
console.log('[TokenStorage] Refresh token expired - clearing');
|
|
12055
|
+
await this.clearRefreshToken();
|
|
12056
|
+
return null;
|
|
12057
|
+
}
|
|
12058
|
+
return rt;
|
|
12059
|
+
},
|
|
12060
|
+
async clearRefreshToken() {
|
|
12061
|
+
const storage = await getStorage();
|
|
12062
|
+
await storage.removeItem(REFRESH_TOKEN_KEY);
|
|
12063
|
+
},
|
|
11999
12064
|
async saveUser(user) {
|
|
12000
12065
|
const storage = await getStorage();
|
|
12001
12066
|
await storage.setItem(USER_KEY, user);
|
|
@@ -12010,6 +12075,7 @@ const tokenStorage = {
|
|
|
12010
12075
|
},
|
|
12011
12076
|
async clearAll() {
|
|
12012
12077
|
await this.clearToken();
|
|
12078
|
+
await this.clearRefreshToken();
|
|
12013
12079
|
await this.clearUser();
|
|
12014
12080
|
await this.clearAccountData();
|
|
12015
12081
|
await this.clearAccountInfo();
|
|
@@ -12067,12 +12133,18 @@ const tokenStorage = {
|
|
|
12067
12133
|
|
|
12068
12134
|
// Export context for optional usage (e.g., SmartlinksFrame can work without AuthProvider)
|
|
12069
12135
|
const AuthContext = createContext(undefined);
|
|
12070
|
-
const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 * 1000, preloadAccountInfo = false,
|
|
12136
|
+
const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 * 1000, preloadAccountInfo = false, clientId: clientIdProp,
|
|
12071
12137
|
// Token refresh settings
|
|
12072
12138
|
enableAutoRefresh = true, refreshThresholdPercent = 75, // Refresh when 75% of token lifetime has passed
|
|
12073
12139
|
refreshCheckInterval = 60 * 1000, // Check every minute
|
|
12140
|
+
refreshOnResume,
|
|
12074
12141
|
// Contact & Interaction features
|
|
12075
12142
|
collectionId, enableContactSync, enableInteractionTracking, interactionAppId, interactionConfig, }) => {
|
|
12143
|
+
// Resolved client ID — explicit prop wins; otherwise fall back to the
|
|
12144
|
+
// collection's defaultAuthKitId (the standard pattern — see
|
|
12145
|
+
// mem://architecture/default-authkit-collection-property).
|
|
12146
|
+
const [resolvedClientId, setResolvedClientId] = useState(clientIdProp);
|
|
12147
|
+
const clientId = resolvedClientId;
|
|
12076
12148
|
const [user, setUser] = useState(null);
|
|
12077
12149
|
const [token, setToken] = useState(null);
|
|
12078
12150
|
const [accountData, setAccountData] = useState(null);
|
|
@@ -12088,8 +12160,8 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
12088
12160
|
const pendingVerificationRef = useRef(false);
|
|
12089
12161
|
const proxyRefreshInFlightRef = useRef(false);
|
|
12090
12162
|
// Stable refs for callbacks to avoid useEffect dependency cycles
|
|
12091
|
-
const syncContactRef = useRef();
|
|
12092
|
-
const trackInteractionRef = useRef();
|
|
12163
|
+
const syncContactRef = useRef(undefined);
|
|
12164
|
+
const trackInteractionRef = useRef(undefined);
|
|
12093
12165
|
// Default to enabled if collectionId is provided
|
|
12094
12166
|
const shouldSyncContacts = enableContactSync ?? !!collectionId;
|
|
12095
12167
|
const shouldTrackInteractions = enableInteractionTracking ?? !!collectionId;
|
|
@@ -12772,9 +12844,7 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
12772
12844
|
});
|
|
12773
12845
|
});
|
|
12774
12846
|
};
|
|
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
|
-
) => {
|
|
12847
|
+
const login = useCallback(async (authToken, authUser, authAccountData, isNewUser, expiresAt, refreshTokenValue, refreshTokenExpiresAt) => {
|
|
12778
12848
|
try {
|
|
12779
12849
|
// Only persist to storage in standalone mode
|
|
12780
12850
|
if (!proxyMode) {
|
|
@@ -12788,6 +12858,15 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
12788
12858
|
else {
|
|
12789
12859
|
await tokenStorage.clearAccountData();
|
|
12790
12860
|
}
|
|
12861
|
+
// Persist refresh token when the backend issued one (native clients).
|
|
12862
|
+
if (refreshTokenValue) {
|
|
12863
|
+
const rtExp = refreshTokenExpiresAt
|
|
12864
|
+
?? Date.now() + 90 * 24 * 60 * 60 * 1000; // 90d fallback per spec
|
|
12865
|
+
await tokenStorage.saveRefreshToken(refreshTokenValue, rtExp);
|
|
12866
|
+
}
|
|
12867
|
+
else {
|
|
12868
|
+
await tokenStorage.clearRefreshToken();
|
|
12869
|
+
}
|
|
12791
12870
|
smartlinks.auth.verifyToken(authToken).catch(() => { });
|
|
12792
12871
|
}
|
|
12793
12872
|
// Always update memory state (but NOT isVerified yet - wait for parent ack in iframe mode)
|
|
@@ -12851,6 +12930,17 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
12851
12930
|
}
|
|
12852
12931
|
// Only clear persistent storage in standalone mode
|
|
12853
12932
|
if (!proxyMode) {
|
|
12933
|
+
// Best-effort: revoke the refresh-token family server-side before
|
|
12934
|
+
// wiping local state, so a stolen copy is dead immediately.
|
|
12935
|
+
try {
|
|
12936
|
+
const storedRefresh = await tokenStorage.getRefreshToken();
|
|
12937
|
+
if (storedRefresh?.token && clientId) {
|
|
12938
|
+
await smartlinks.authKit.logout(clientId, storedRefresh.token);
|
|
12939
|
+
}
|
|
12940
|
+
}
|
|
12941
|
+
catch (err) {
|
|
12942
|
+
console.warn('[AuthContext] Refresh-token revoke failed (continuing):', err);
|
|
12943
|
+
}
|
|
12854
12944
|
await tokenStorage.clearAll();
|
|
12855
12945
|
smartlinks.auth.logout();
|
|
12856
12946
|
}
|
|
@@ -12876,7 +12966,7 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
12876
12966
|
catch (error) {
|
|
12877
12967
|
console.error('Failed to clear auth data from storage:', error);
|
|
12878
12968
|
}
|
|
12879
|
-
}, [proxyMode, notifyAuthStateChange, user, contactId, collectionId, shouldTrackInteractions, trackInteraction]);
|
|
12969
|
+
}, [proxyMode, clientId, notifyAuthStateChange, user, contactId, collectionId, shouldTrackInteractions, trackInteraction]);
|
|
12880
12970
|
const getToken = useCallback(async () => {
|
|
12881
12971
|
if (proxyMode) {
|
|
12882
12972
|
return token;
|
|
@@ -12903,12 +12993,44 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
12903
12993
|
expiresIn: Math.max(0, storedToken.expiresAt - Date.now()),
|
|
12904
12994
|
};
|
|
12905
12995
|
}, [proxyMode, token]);
|
|
12906
|
-
// Refresh token -
|
|
12996
|
+
// Refresh token - prefers the AuthKit refresh-token endpoint when a refresh
|
|
12997
|
+
// token is stored (native clients); otherwise falls back to verify-and-extend
|
|
12998
|
+
// for web clients whose backend still issues only short-lived JWTs.
|
|
12907
12999
|
const refreshToken = useCallback(async () => {
|
|
12908
13000
|
if (proxyMode) {
|
|
12909
13001
|
throw new Error('Token refresh in proxy mode is handled by the parent application');
|
|
12910
13002
|
}
|
|
12911
13003
|
const storedToken = await tokenStorage.getToken();
|
|
13004
|
+
const storedRefresh = await tokenStorage.getRefreshToken();
|
|
13005
|
+
// -------- Path A: true refresh-token rotation (native) ---------------
|
|
13006
|
+
if (storedRefresh?.token && clientId) {
|
|
13007
|
+
try {
|
|
13008
|
+
const resp = await smartlinks.authKit.refreshToken(clientId, storedRefresh.token);
|
|
13009
|
+
await tokenStorage.saveToken(resp.token, resp.expiresAt);
|
|
13010
|
+
await tokenStorage.saveRefreshToken(resp.refreshToken, resp.refreshTokenExpiresAt);
|
|
13011
|
+
setToken(resp.token);
|
|
13012
|
+
setIsVerified(true);
|
|
13013
|
+
pendingVerificationRef.current = false;
|
|
13014
|
+
notifyAuthStateChange('TOKEN_REFRESH', user, resp.token, accountData, accountInfo, true, contact, contactId);
|
|
13015
|
+
return resp.token;
|
|
13016
|
+
}
|
|
13017
|
+
catch (error) {
|
|
13018
|
+
console.error('[AuthContext] Refresh-token rotation failed:', error);
|
|
13019
|
+
if (isNetworkError(error))
|
|
13020
|
+
throw error;
|
|
13021
|
+
// Reuse / invalid / expired → force re-login. The SDK surfaces these
|
|
13022
|
+
// as RefreshErrorCode (INVALID_REFRESH_TOKEN / REUSE_DETECTED / MISSING).
|
|
13023
|
+
const code = error?.errorCode ?? error?.code;
|
|
13024
|
+
if (code === 'INVALID_REFRESH_TOKEN' ||
|
|
13025
|
+
code === 'REFRESH_TOKEN_REUSE_DETECTED' ||
|
|
13026
|
+
code === 'MISSING_REFRESH_TOKEN') {
|
|
13027
|
+
await logout();
|
|
13028
|
+
throw new Error('Session expired. Please login again.');
|
|
13029
|
+
}
|
|
13030
|
+
// Fall through to verify-and-extend on unexpected errors.
|
|
13031
|
+
}
|
|
13032
|
+
}
|
|
13033
|
+
// -------- Path B: legacy verify-and-extend (web) ---------------------
|
|
12912
13034
|
if (!storedToken?.token) {
|
|
12913
13035
|
throw new Error('No token to refresh. Please login first.');
|
|
12914
13036
|
}
|
|
@@ -12933,7 +13055,7 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
12933
13055
|
await logout();
|
|
12934
13056
|
throw new Error('Token refresh failed. Please login again.');
|
|
12935
13057
|
}
|
|
12936
|
-
}, [proxyMode, user, accountData, accountInfo, contact, contactId, notifyAuthStateChange, isNetworkError, logout]);
|
|
13058
|
+
}, [proxyMode, clientId, user, accountData, accountInfo, contact, contactId, notifyAuthStateChange, isNetworkError, logout]);
|
|
12937
13059
|
const getAccount = useCallback(async (forceRefresh = false) => {
|
|
12938
13060
|
try {
|
|
12939
13061
|
if (proxyMode) {
|
|
@@ -13097,6 +13219,105 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
13097
13219
|
clearInterval(intervalId);
|
|
13098
13220
|
};
|
|
13099
13221
|
}, [proxyMode, enableAutoRefresh, refreshCheckInterval, refreshThresholdPercent, token, user, refreshToken]);
|
|
13222
|
+
// Resolve clientId from the collection's defaultAuthKitId when not provided
|
|
13223
|
+
// explicitly. Most apps only pass `collectionId` — this means refresh-token
|
|
13224
|
+
// rotation and server-side logout revocation still work end-to-end.
|
|
13225
|
+
useEffect(() => {
|
|
13226
|
+
if (clientIdProp) {
|
|
13227
|
+
setResolvedClientId(clientIdProp);
|
|
13228
|
+
return;
|
|
13229
|
+
}
|
|
13230
|
+
if (!collectionId)
|
|
13231
|
+
return;
|
|
13232
|
+
let cancelled = false;
|
|
13233
|
+
(async () => {
|
|
13234
|
+
try {
|
|
13235
|
+
const { getDefaultAuthKitId } = await Promise.resolve().then(function () { return defaultAuthKit; });
|
|
13236
|
+
const id = await getDefaultAuthKitId(collectionId);
|
|
13237
|
+
if (!cancelled && id)
|
|
13238
|
+
setResolvedClientId(id);
|
|
13239
|
+
}
|
|
13240
|
+
catch (err) {
|
|
13241
|
+
console.warn('[AuthContext] Could not resolve default authKitId from collection:', err);
|
|
13242
|
+
}
|
|
13243
|
+
})();
|
|
13244
|
+
return () => { cancelled = true; };
|
|
13245
|
+
}, [clientIdProp, collectionId]);
|
|
13246
|
+
// Refresh-on-resume: fires when the app returns to the foreground.
|
|
13247
|
+
// - Capacitor: `App.addListener('appStateChange')` via dynamic import (no
|
|
13248
|
+
// runtime dep on web).
|
|
13249
|
+
// - Universal fallback: `document.visibilitychange` — also catches PWAs /
|
|
13250
|
+
// web tabs that have been backgrounded for hours.
|
|
13251
|
+
useEffect(() => {
|
|
13252
|
+
if (proxyMode || !enableAutoRefresh || !token || !user)
|
|
13253
|
+
return;
|
|
13254
|
+
// Default ON when we detect a native shell; OFF otherwise.
|
|
13255
|
+
const isNative = (() => {
|
|
13256
|
+
try {
|
|
13257
|
+
const cap = window.Capacitor;
|
|
13258
|
+
if (cap?.isNativePlatform?.() === true)
|
|
13259
|
+
return true;
|
|
13260
|
+
if (window.AuthKit?.isNative === true)
|
|
13261
|
+
return true;
|
|
13262
|
+
}
|
|
13263
|
+
catch { /* noop */ }
|
|
13264
|
+
return false;
|
|
13265
|
+
})();
|
|
13266
|
+
const enabled = refreshOnResume ?? isNative;
|
|
13267
|
+
if (!enabled)
|
|
13268
|
+
return;
|
|
13269
|
+
const maybeRefresh = async () => {
|
|
13270
|
+
try {
|
|
13271
|
+
const storedToken = await tokenStorage.getToken();
|
|
13272
|
+
if (!storedToken?.expiresAt)
|
|
13273
|
+
return;
|
|
13274
|
+
const remainingMs = storedToken.expiresAt - Date.now();
|
|
13275
|
+
const totalLifetimeMs = 7 * 24 * 60 * 60 * 1000;
|
|
13276
|
+
const percentUsed = ((totalLifetimeMs - remainingMs) / totalLifetimeMs) * 100;
|
|
13277
|
+
if (percentUsed < refreshThresholdPercent)
|
|
13278
|
+
return;
|
|
13279
|
+
await refreshToken().catch(() => { });
|
|
13280
|
+
}
|
|
13281
|
+
catch (err) {
|
|
13282
|
+
console.error('[AuthContext] Resume refresh check failed:', err);
|
|
13283
|
+
}
|
|
13284
|
+
};
|
|
13285
|
+
// Universal visibility listener
|
|
13286
|
+
const onVisibility = () => {
|
|
13287
|
+
if (document.visibilityState === 'visible')
|
|
13288
|
+
maybeRefresh();
|
|
13289
|
+
};
|
|
13290
|
+
document.addEventListener('visibilitychange', onVisibility);
|
|
13291
|
+
// Capacitor listener (optional)
|
|
13292
|
+
let capRemove;
|
|
13293
|
+
(async () => {
|
|
13294
|
+
try {
|
|
13295
|
+
// Only attempt to load @capacitor/app in an actual native shell.
|
|
13296
|
+
// The specifier is obfuscated so bundlers (Vite/webpack) don't try
|
|
13297
|
+
// to statically resolve an optional peer dependency at build time.
|
|
13298
|
+
const isNative = typeof window !== 'undefined' &&
|
|
13299
|
+
(window.Capacitor?.isNativePlatform?.() === true ||
|
|
13300
|
+
window.AuthKit?.isNative === true);
|
|
13301
|
+
if (!isNative)
|
|
13302
|
+
return;
|
|
13303
|
+
const specifier = ['@capacitor', 'app'].join('/');
|
|
13304
|
+
const dynamicImport = new Function('s', 'return import(s)');
|
|
13305
|
+
const mod = await dynamicImport(specifier);
|
|
13306
|
+
const handle = await mod.App.addListener('appStateChange', (state) => {
|
|
13307
|
+
if (state.isActive)
|
|
13308
|
+
maybeRefresh();
|
|
13309
|
+
});
|
|
13310
|
+
capRemove = () => handle?.remove?.();
|
|
13311
|
+
}
|
|
13312
|
+
catch {
|
|
13313
|
+
// @capacitor/app not installed or unavailable — visibilitychange handles it.
|
|
13314
|
+
}
|
|
13315
|
+
})();
|
|
13316
|
+
return () => {
|
|
13317
|
+
document.removeEventListener('visibilitychange', onVisibility);
|
|
13318
|
+
capRemove?.();
|
|
13319
|
+
};
|
|
13320
|
+
}, [proxyMode, enableAutoRefresh, refreshOnResume, refreshThresholdPercent, token, user, refreshToken]);
|
|
13100
13321
|
const value = {
|
|
13101
13322
|
user,
|
|
13102
13323
|
token,
|
|
@@ -13358,8 +13579,22 @@ const loadGoogleIdentityServices = () => {
|
|
|
13358
13579
|
document.head.appendChild(script);
|
|
13359
13580
|
});
|
|
13360
13581
|
};
|
|
13582
|
+
// Helper to detect a Capacitor native runtime (iOS/Android shell, not the web)
|
|
13583
|
+
const isCapacitorNative = () => {
|
|
13584
|
+
const cap = window.Capacitor;
|
|
13585
|
+
if (!cap)
|
|
13586
|
+
return false;
|
|
13587
|
+
// isNativePlatform() is the modern API; fall back to platform string for older cores
|
|
13588
|
+
if (typeof cap.isNativePlatform === 'function')
|
|
13589
|
+
return cap.isNativePlatform();
|
|
13590
|
+
return cap.platform === 'ios' || cap.platform === 'android';
|
|
13591
|
+
};
|
|
13361
13592
|
// Helper to detect WebView environments (Android/iOS)
|
|
13362
13593
|
const detectWebView = () => {
|
|
13594
|
+
// Capacitor apps run inside a WebView (WKWebView/Android WebView) but don't
|
|
13595
|
+
// always set the `wv` UA token — treat them as WebView explicitly.
|
|
13596
|
+
if (isCapacitorNative())
|
|
13597
|
+
return true;
|
|
13363
13598
|
const ua = navigator.userAgent;
|
|
13364
13599
|
// Android WebView detection
|
|
13365
13600
|
if (/Android/i.test(ua)) {
|
|
@@ -13467,7 +13702,7 @@ const checkSilentGoogleSignIn = async (clientId, googleClientId) => {
|
|
|
13467
13702
|
});
|
|
13468
13703
|
};
|
|
13469
13704
|
// 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, }) => {
|
|
13705
|
+
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
13706
|
// Resolve signup prominence from props, customization, config, or default
|
|
13472
13707
|
const resolvedSignupProminence = signupProminence || customization?.signupProminence || 'minimal';
|
|
13473
13708
|
// Determine initial mode based on signupProminence setting
|
|
@@ -13540,17 +13775,48 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13540
13775
|
}
|
|
13541
13776
|
};
|
|
13542
13777
|
// Dark mode detection and theme management
|
|
13778
|
+
// In 'auto' mode we honor (in priority order):
|
|
13779
|
+
// 1. An explicit `.dark` class on <html> or <body> (set by the host portal / SmartLinks theme system)
|
|
13780
|
+
// 2. A SmartLinks theme payload in the URL (?theme=... with m === 'd')
|
|
13781
|
+
// 3. The OS `prefers-color-scheme` preference
|
|
13543
13782
|
useEffect(() => {
|
|
13544
13783
|
if (theme !== 'auto') {
|
|
13545
13784
|
setResolvedTheme(theme);
|
|
13546
13785
|
return;
|
|
13547
13786
|
}
|
|
13548
|
-
// Auto-detect system theme preference
|
|
13549
13787
|
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
13550
|
-
const
|
|
13788
|
+
const hostHasDarkClass = () => document.documentElement.classList.contains('dark') ||
|
|
13789
|
+
document.body.classList.contains('dark');
|
|
13790
|
+
const urlRequestsDark = () => {
|
|
13791
|
+
try {
|
|
13792
|
+
const search = new URLSearchParams(window.location.search);
|
|
13793
|
+
let themeParam = search.get('theme');
|
|
13794
|
+
if (!themeParam && window.location.hash.includes('?')) {
|
|
13795
|
+
themeParam = new URLSearchParams(window.location.hash.substring(window.location.hash.indexOf('?'))).get('theme');
|
|
13796
|
+
}
|
|
13797
|
+
if (!themeParam)
|
|
13798
|
+
return false;
|
|
13799
|
+
const decoded = JSON.parse(atob(themeParam));
|
|
13800
|
+
return decoded?.m === 'd';
|
|
13801
|
+
}
|
|
13802
|
+
catch {
|
|
13803
|
+
return false;
|
|
13804
|
+
}
|
|
13805
|
+
};
|
|
13806
|
+
const updateTheme = () => {
|
|
13807
|
+
const isDark = hostHasDarkClass() || urlRequestsDark() || mediaQuery.matches;
|
|
13808
|
+
setResolvedTheme(isDark ? 'dark' : 'light');
|
|
13809
|
+
};
|
|
13551
13810
|
updateTheme();
|
|
13552
13811
|
mediaQuery.addEventListener('change', updateTheme);
|
|
13553
|
-
|
|
13812
|
+
// Watch host <html>/<body> class changes (portal toggling dark mode at runtime)
|
|
13813
|
+
const observer = new MutationObserver(updateTheme);
|
|
13814
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
|
13815
|
+
observer.observe(document.body, { attributes: true, attributeFilter: ['class'] });
|
|
13816
|
+
return () => {
|
|
13817
|
+
mediaQuery.removeEventListener('change', updateTheme);
|
|
13818
|
+
observer.disconnect();
|
|
13819
|
+
};
|
|
13554
13820
|
}, [theme]);
|
|
13555
13821
|
// Version tracking for debugging if needed
|
|
13556
13822
|
// console.log(`${LOG_PREFIX} Component mounted, v${AUTH_UI_VERSION}`);
|
|
@@ -13736,23 +14002,28 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13736
14002
|
};
|
|
13737
14003
|
fetchSchema();
|
|
13738
14004
|
}, [collectionId, sdkReady]);
|
|
13739
|
-
// Silent
|
|
13740
|
-
//
|
|
14005
|
+
// Silent Sign-In check on mount (for native apps).
|
|
14006
|
+
// Prefers the injected `nativeAuth.checkSignIn` adapter (Capacitor) when
|
|
14007
|
+
// `enableSilentNativeSignIn` is set; otherwise falls back to the legacy
|
|
14008
|
+
// `window.AuthKit` bridge when `enableSilentGoogleSignIn` is set.
|
|
14009
|
+
const wantsSilentNative = enableSilentNativeSignIn && !!nativeAuth?.checkSignIn;
|
|
13741
14010
|
useEffect(() => {
|
|
13742
|
-
if (!enableSilentGoogleSignIn || silentSignInChecked || !sdkReady || auth.isAuthenticated) {
|
|
14011
|
+
if ((!enableSilentGoogleSignIn && !wantsSilentNative) || silentSignInChecked || !sdkReady || auth.isAuthenticated) {
|
|
13743
14012
|
return;
|
|
13744
14013
|
}
|
|
13745
14014
|
const googleClientId = config?.googleClientId || DEFAULT_GOOGLE_CLIENT_ID;
|
|
13746
14015
|
const performSilentSignIn = async () => {
|
|
13747
14016
|
try {
|
|
13748
|
-
const result =
|
|
14017
|
+
const result = wantsSilentNative
|
|
14018
|
+
? await nativeAuth.checkSignIn({ serverClientId: googleClientId })
|
|
14019
|
+
: await checkSilentGoogleSignIn(clientId, googleClientId);
|
|
13749
14020
|
setSilentSignInChecked(true);
|
|
13750
14021
|
if (result?.isSignedIn && result.idToken) {
|
|
13751
14022
|
setLoading(true);
|
|
13752
14023
|
try {
|
|
13753
14024
|
const authResponse = await api.loginWithGoogle(result.idToken);
|
|
13754
14025
|
if (authResponse.token) {
|
|
13755
|
-
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, false, getExpirationFromResponse(authResponse));
|
|
14026
|
+
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, false, getExpirationFromResponse(authResponse), authResponse.refreshToken, authResponse.refreshTokenExpiresAt);
|
|
13756
14027
|
setAuthSuccess(true);
|
|
13757
14028
|
setSuccessMessage('Signed in automatically with Google!');
|
|
13758
14029
|
onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
|
|
@@ -13771,7 +14042,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13771
14042
|
}
|
|
13772
14043
|
};
|
|
13773
14044
|
performSilentSignIn();
|
|
13774
|
-
}, [enableSilentGoogleSignIn, silentSignInChecked, sdkReady, auth.isAuthenticated, clientId, config?.googleClientId, api, auth, onAuthSuccess]);
|
|
14045
|
+
}, [enableSilentGoogleSignIn, wantsSilentNative, nativeAuth, silentSignInChecked, sdkReady, auth.isAuthenticated, clientId, config?.googleClientId, api, auth, onAuthSuccess]);
|
|
13775
14046
|
// Reset showEmailForm when mode changes away from login/register
|
|
13776
14047
|
useEffect(() => {
|
|
13777
14048
|
if (mode !== 'login' && mode !== 'register') {
|
|
@@ -13847,7 +14118,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13847
14118
|
if ((verificationMode === 'verify-auto-login' || verificationMode === 'immediate') && response.token) {
|
|
13848
14119
|
// Auto-login modes: Log the user in immediately if token is provided
|
|
13849
14120
|
// Always await - auth.login now waits for parent ack automatically in iframe mode
|
|
13850
|
-
await auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response));
|
|
14121
|
+
await auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response), response.refreshToken, response.refreshTokenExpiresAt);
|
|
13851
14122
|
setUrlAuthProcessing(false); // Clear processing state before redirect/success
|
|
13852
14123
|
if (redirectUrl) {
|
|
13853
14124
|
// Redirect to clean URL and resume flow
|
|
@@ -13907,7 +14178,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13907
14178
|
// Auto-login with magic link if token is provided
|
|
13908
14179
|
if (response.token) {
|
|
13909
14180
|
// Always await - auth.login now waits for parent ack automatically in iframe mode
|
|
13910
|
-
await auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response));
|
|
14181
|
+
await auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response), response.refreshToken, response.refreshTokenExpiresAt);
|
|
13911
14182
|
setUrlAuthProcessing(false); // Clear processing state before redirect/success
|
|
13912
14183
|
if (redirectUrl) {
|
|
13913
14184
|
// Redirect to clean URL and resume flow
|
|
@@ -13982,7 +14253,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13982
14253
|
log.log('Google OAuth code exchange response:', { hasToken: !!response.token, hasUser: !!response.user, isNewUser: response.isNewUser });
|
|
13983
14254
|
if (response.token) {
|
|
13984
14255
|
// Await login to ensure token is persisted before any navigation
|
|
13985
|
-
await auth.login(response.token, response.user, response.accountData, response.isNewUser, getExpirationFromResponse(response));
|
|
14256
|
+
await auth.login(response.token, response.user, response.accountData, response.isNewUser, getExpirationFromResponse(response), response.refreshToken, response.refreshTokenExpiresAt);
|
|
13986
14257
|
setAuthSuccess(true);
|
|
13987
14258
|
setSuccessMessage('Google login successful!');
|
|
13988
14259
|
onAuthSuccess(response.token, response.user, response.accountData);
|
|
@@ -14036,7 +14307,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14036
14307
|
// Handle different verification modes
|
|
14037
14308
|
if (verificationMode === 'immediate' && response.token) {
|
|
14038
14309
|
// Immediate mode: Log in right away if token is provided (isNewUser=true for registration)
|
|
14039
|
-
await auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response));
|
|
14310
|
+
await auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response), response.refreshToken, response.refreshTokenExpiresAt);
|
|
14040
14311
|
setAuthSuccess(true);
|
|
14041
14312
|
const deadline = response.emailVerificationDeadline
|
|
14042
14313
|
? new Date(response.emailVerificationDeadline).toLocaleString()
|
|
@@ -14098,7 +14369,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14098
14369
|
setLoading(false);
|
|
14099
14370
|
return;
|
|
14100
14371
|
}
|
|
14101
|
-
await auth.login(response.token, response.user, response.accountData, false, getExpirationFromResponse(response));
|
|
14372
|
+
await auth.login(response.token, response.user, response.accountData, false, getExpirationFromResponse(response), response.refreshToken, response.refreshTokenExpiresAt);
|
|
14102
14373
|
setAuthSuccess(true);
|
|
14103
14374
|
setSuccessMessage('Login successful!');
|
|
14104
14375
|
onAuthSuccess(response.token, response.user, response.accountData);
|
|
@@ -14228,6 +14499,55 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14228
14499
|
}
|
|
14229
14500
|
});
|
|
14230
14501
|
};
|
|
14502
|
+
// Finish a login once a backend AuthResponse has been obtained (shared by the
|
|
14503
|
+
// native adapter paths for Google and Apple).
|
|
14504
|
+
const completeNativeLogin = async (authResponse, successLabel) => {
|
|
14505
|
+
if (!authResponse.token) {
|
|
14506
|
+
throw new Error('Authentication failed - no token received');
|
|
14507
|
+
}
|
|
14508
|
+
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse));
|
|
14509
|
+
setAuthSuccess(true);
|
|
14510
|
+
setSuccessMessage(successLabel);
|
|
14511
|
+
onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
|
|
14512
|
+
};
|
|
14513
|
+
// Sign in with Apple. Apple is native-only in this kit today: it requires an
|
|
14514
|
+
// injected `nativeAuth` adapter (e.g. a Capacitor plugin) that resolves an
|
|
14515
|
+
// Apple identity token. There is no in-browser fallback yet.
|
|
14516
|
+
const handleAppleLogin = async () => {
|
|
14517
|
+
if (!nativeAuth?.signInWithApple) {
|
|
14518
|
+
log.warn('Apple sign-in requested but no nativeAuth.signInWithApple adapter is configured');
|
|
14519
|
+
setError('Apple Sign-In is not available in this environment.');
|
|
14520
|
+
onAuthError?.(new Error('No nativeAuth.signInWithApple adapter configured'));
|
|
14521
|
+
return;
|
|
14522
|
+
}
|
|
14523
|
+
setLoading(true);
|
|
14524
|
+
setError(undefined);
|
|
14525
|
+
try {
|
|
14526
|
+
log.log('Using native adapter for Apple Sign-In');
|
|
14527
|
+
const result = await nativeAuth.signInWithApple({
|
|
14528
|
+
clientId: config?.appleClientId,
|
|
14529
|
+
scopes: ['name', 'email'],
|
|
14530
|
+
});
|
|
14531
|
+
if (!result?.idToken) {
|
|
14532
|
+
throw new Error('Apple Sign-In did not return an identity token');
|
|
14533
|
+
}
|
|
14534
|
+
const authResponse = await api.loginWithApple(result.idToken, {
|
|
14535
|
+
authorizationCode: result.authorizationCode,
|
|
14536
|
+
nonce: result.nonce,
|
|
14537
|
+
appleUserInfo: result.email || result.name ? { email: result.email, name: result.name } : undefined,
|
|
14538
|
+
});
|
|
14539
|
+
log.log('Native Apple login response:', { hasToken: !!authResponse.token, isNewUser: authResponse.isNewUser });
|
|
14540
|
+
await completeNativeLogin(authResponse, 'Apple login successful!');
|
|
14541
|
+
}
|
|
14542
|
+
catch (err) {
|
|
14543
|
+
log.error('Apple Sign-In failed:', err);
|
|
14544
|
+
setError(getFriendlyErrorMessage(err));
|
|
14545
|
+
onAuthError?.(err instanceof Error ? err : new Error(getFriendlyErrorMessage(err)));
|
|
14546
|
+
}
|
|
14547
|
+
finally {
|
|
14548
|
+
setLoading(false);
|
|
14549
|
+
}
|
|
14550
|
+
};
|
|
14231
14551
|
const handleGoogleLogin = async () => {
|
|
14232
14552
|
const hasCustomGoogleClientId = !!config?.googleClientId;
|
|
14233
14553
|
const googleClientId = config?.googleClientId || DEFAULT_GOOGLE_CLIENT_ID;
|
|
@@ -14257,6 +14577,32 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14257
14577
|
setLoading(true);
|
|
14258
14578
|
setError(undefined);
|
|
14259
14579
|
try {
|
|
14580
|
+
// Priority 0: injected Promise-based native adapter (Capacitor, etc.).
|
|
14581
|
+
// This is the clean path — await the plugin directly, no callback bridge.
|
|
14582
|
+
if (nativeAuth?.signInWithGoogle) {
|
|
14583
|
+
log.log('Using injected nativeAuth adapter for Google Sign-In');
|
|
14584
|
+
try {
|
|
14585
|
+
const result = await nativeAuth.signInWithGoogle({
|
|
14586
|
+
serverClientId: googleClientId,
|
|
14587
|
+
scopes: ['email', 'profile'],
|
|
14588
|
+
});
|
|
14589
|
+
if (!result?.idToken) {
|
|
14590
|
+
throw new Error('Google Sign-In did not return an ID token');
|
|
14591
|
+
}
|
|
14592
|
+
const authResponse = await api.loginWithGoogle(result.idToken);
|
|
14593
|
+
log.log('Native (adapter) Google login response:', { hasToken: !!authResponse.token, isNewUser: authResponse.isNewUser });
|
|
14594
|
+
await completeNativeLogin(authResponse, 'Google login successful!');
|
|
14595
|
+
}
|
|
14596
|
+
catch (err) {
|
|
14597
|
+
log.error('Adapter Google Sign-In failed:', err);
|
|
14598
|
+
setError(getFriendlyErrorMessage(err));
|
|
14599
|
+
onAuthError?.(err instanceof Error ? err : new Error(getFriendlyErrorMessage(err)));
|
|
14600
|
+
}
|
|
14601
|
+
finally {
|
|
14602
|
+
setLoading(false);
|
|
14603
|
+
}
|
|
14604
|
+
return;
|
|
14605
|
+
}
|
|
14260
14606
|
if (nativeBridge) {
|
|
14261
14607
|
log.log('Using native bridge for Google Sign-In');
|
|
14262
14608
|
const callbackId = `google_auth_${Date.now()}`;
|
|
@@ -14275,7 +14621,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14275
14621
|
const authResponse = await api.loginWithGoogle(result.idToken);
|
|
14276
14622
|
log.log('Native Google login response:', { hasToken: !!authResponse.token, hasUser: !!authResponse.user, isNewUser: authResponse.isNewUser });
|
|
14277
14623
|
if (authResponse.token) {
|
|
14278
|
-
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse));
|
|
14624
|
+
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse), authResponse.refreshToken, authResponse.refreshTokenExpiresAt);
|
|
14279
14625
|
setAuthSuccess(true);
|
|
14280
14626
|
setSuccessMessage('Google login successful!');
|
|
14281
14627
|
onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
|
|
@@ -14495,7 +14841,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14495
14841
|
log.log('Popup Google login response:', { hasToken: !!authResponse.token, hasUser: !!authResponse.user, isNewUser: authResponse.isNewUser });
|
|
14496
14842
|
if (authResponse.token) {
|
|
14497
14843
|
// Google OAuth can be login or signup - use isNewUser flag from backend if available
|
|
14498
|
-
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse));
|
|
14844
|
+
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse), authResponse.refreshToken, authResponse.refreshTokenExpiresAt);
|
|
14499
14845
|
setAuthSuccess(true);
|
|
14500
14846
|
setSuccessMessage('Google login successful!');
|
|
14501
14847
|
onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
|
|
@@ -14545,7 +14891,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14545
14891
|
log.log('OneTap Google login response:', { hasToken: !!authResponse.token, hasUser: !!authResponse.user, isNewUser: authResponse.isNewUser });
|
|
14546
14892
|
if (authResponse.token) {
|
|
14547
14893
|
// Google OAuth can be login or signup - use isNewUser flag from backend if available
|
|
14548
|
-
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse));
|
|
14894
|
+
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse), authResponse.refreshToken, authResponse.refreshTokenExpiresAt);
|
|
14549
14895
|
setAuthSuccess(true);
|
|
14550
14896
|
setSuccessMessage('Google login successful!');
|
|
14551
14897
|
onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
|
|
@@ -14627,7 +14973,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14627
14973
|
// Phone auth is an INTERACTIVE flow (user entered OTP in UI)
|
|
14628
14974
|
// Unlike deep-link flows (email verification, magic link), there's no URL token to clean up
|
|
14629
14975
|
// Do NOT auto-redirect - let the parent app control the next step (profile completion, etc.)
|
|
14630
|
-
await auth.login(response.token, response.user, response.accountData, response.isNewUser, getExpirationFromResponse(response));
|
|
14976
|
+
await auth.login(response.token, response.user, response.accountData, response.isNewUser, getExpirationFromResponse(response), response.refreshToken, response.refreshTokenExpiresAt);
|
|
14631
14977
|
setAuthSuccess(true);
|
|
14632
14978
|
setSuccessMessage('Phone verified! You are now logged in.');
|
|
14633
14979
|
onAuthSuccess(response.token, response.user, response.accountData);
|
|
@@ -14917,7 +15263,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14917
15263
|
throw Object.assign(new Error(resultErrorMessage), session);
|
|
14918
15264
|
}
|
|
14919
15265
|
if (session?.token && session.user) {
|
|
14920
|
-
await auth.login(session.token, session.user, session.accountData, true, getExpirationFromResponse(session));
|
|
15266
|
+
await auth.login(session.token, session.user, session.accountData, true, getExpirationFromResponse(session), session.refreshToken, session.refreshTokenExpiresAt);
|
|
14921
15267
|
if (!proxyMode) {
|
|
14922
15268
|
onAuthSuccess(session.token, session.user, session.accountData);
|
|
14923
15269
|
}
|
|
@@ -15247,11 +15593,11 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
15247
15593
|
const hasEmailProvider = actualProviders.includes('email');
|
|
15248
15594
|
// If email provider is not enabled, only show provider buttons (no email/password form)
|
|
15249
15595
|
if (!hasEmailProvider) {
|
|
15250
|
-
return (jsx(ProviderButtons, { enabledProviders: actualProviders, providerOrder: providerOrder, onGoogleLogin: handleGoogleLogin, onPhoneLogin: () => setMode('phone'), onMagicLinkLogin: () => setMode('magic-link'), onWhatsAppLogin: () => setMode('whatsapp'), loading: loading }));
|
|
15596
|
+
return (jsx(ProviderButtons, { enabledProviders: actualProviders, providerOrder: providerOrder, onGoogleLogin: handleGoogleLogin, onAppleLogin: handleAppleLogin, onPhoneLogin: () => setMode('phone'), onMagicLinkLogin: () => setMode('magic-link'), onWhatsAppLogin: () => setMode('whatsapp'), loading: loading }));
|
|
15251
15597
|
}
|
|
15252
15598
|
// Button mode: show provider selection first, then email form if email is selected
|
|
15253
15599
|
if (emailDisplayMode === 'button' && !showEmailForm) {
|
|
15254
|
-
return (jsx(ProviderButtons, { enabledProviders: actualProviders, providerOrder: providerOrder, onEmailLogin: () => setShowEmailForm(true), onGoogleLogin: handleGoogleLogin, onPhoneLogin: () => setMode('phone'), onMagicLinkLogin: () => setMode('magic-link'), onWhatsAppLogin: () => setMode('whatsapp'), loading: loading }));
|
|
15600
|
+
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 }));
|
|
15255
15601
|
}
|
|
15256
15602
|
// Form mode or email button was clicked: show email form with other providers
|
|
15257
15603
|
return (jsxs(Fragment, { children: [emailDisplayMode === 'button' && showEmailForm && (jsx("button", { onClick: () => setShowEmailForm(false), style: {
|
|
@@ -15270,7 +15616,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
15270
15616
|
setShowResendVerification(false);
|
|
15271
15617
|
setShowRequestNewReset(false);
|
|
15272
15618
|
setError(undefined);
|
|
15273
|
-
}, 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 }))] }));
|
|
15619
|
+
}, 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 }))] }));
|
|
15274
15620
|
})()] })) })) : null }));
|
|
15275
15621
|
};
|
|
15276
15622
|
|
|
@@ -16411,5 +16757,11 @@ async function setDefaultAuthKitId(collectionId, authKitId) {
|
|
|
16411
16757
|
});
|
|
16412
16758
|
}
|
|
16413
16759
|
|
|
16414
|
-
|
|
16760
|
+
var defaultAuthKit = /*#__PURE__*/Object.freeze({
|
|
16761
|
+
__proto__: null,
|
|
16762
|
+
getDefaultAuthKitId: getDefaultAuthKitId,
|
|
16763
|
+
setDefaultAuthKitId: setDefaultAuthKitId
|
|
16764
|
+
});
|
|
16765
|
+
|
|
16766
|
+
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 };
|
|
16415
16767
|
//# sourceMappingURL=index.esm.js.map
|