@proveanything/smartlinks-auth-ui 0.3.10 → 0.3.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/AccountManagement.d.ts.map +1 -1
- package/dist/components/SmartlinksAuthUI.d.ts.map +1 -1
- package/dist/context/AuthContext.d.ts +1 -31
- package/dist/context/AuthContext.d.ts.map +1 -1
- package/dist/index.esm.js +151 -25
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +151 -25
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +26 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AccountManagement.d.ts","sourceRoot":"","sources":["../../src/components/AccountManagement.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA2C,MAAM,OAAO,CAAC;AAMhE,OAAO,KAAK,EAAE,sBAAsB,EAAe,MAAM,UAAU,CAAC;AACpE,OAAO,qBAAqB,CAAC;AAO7B,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC,sBAAsB,
|
|
1
|
+
{"version":3,"file":"AccountManagement.d.ts","sourceRoot":"","sources":["../../src/components/AccountManagement.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA2C,MAAM,OAAO,CAAC;AAMhE,OAAO,KAAK,EAAE,sBAAsB,EAAe,MAAM,UAAU,CAAC;AACpE,OAAO,qBAAqB,CAAC;AAO7B,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC,sBAAsB,CA0wB9D,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SmartlinksAuthUI.d.ts","sourceRoot":"","sources":["../../src/components/SmartlinksAuthUI.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAuC,MAAM,OAAO,CAAC;AAY5D,OAAO,KAAK,EAAE,qBAAqB,EAAyF,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"SmartlinksAuthUI.d.ts","sourceRoot":"","sources":["../../src/components/SmartlinksAuthUI.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAuC,MAAM,OAAO,CAAC;AAY5D,OAAO,KAAK,EAAE,qBAAqB,EAAyF,MAAM,UAAU,CAAC;AA0I7I,QAAA,MAAM,mBAAmB,QAAa,OAAO,CAAC,IAAI,CAmCjD,CAAC;AAwEF,OAAO,EAAE,mBAAmB,EAAE,CAAC;AAK/B,eAAO,MAAM,gBAAgB,EAAE,KAAK,CAAC,EAAE,CAAC,qBAAqB,CA0mD5D,CAAC"}
|
|
@@ -1,35 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import type {
|
|
3
|
-
import type { AuthUser, AuthStateChangeCallback, AuthProviderProps } from '../types';
|
|
4
|
-
interface AuthContextValue {
|
|
5
|
-
user: AuthUser | null;
|
|
6
|
-
token: string | null;
|
|
7
|
-
accountData: Record<string, any> | null;
|
|
8
|
-
accountInfo: Record<string, any> | null;
|
|
9
|
-
isAuthenticated: boolean;
|
|
10
|
-
isVerified: boolean;
|
|
11
|
-
isLoading: boolean;
|
|
12
|
-
isOnline: boolean;
|
|
13
|
-
proxyMode: boolean;
|
|
14
|
-
contact: ContactResponse | null;
|
|
15
|
-
contactId: string | null;
|
|
16
|
-
getContact: (forceRefresh?: boolean) => Promise<ContactResponse | null>;
|
|
17
|
-
updateContactCustomFields: (customFields: Record<string, any>) => Promise<ContactResponse>;
|
|
18
|
-
login: (token: string, user: AuthUser, accountData?: Record<string, any>, isNewUser?: boolean, expiresAt?: number) => Promise<void>;
|
|
19
|
-
logout: () => Promise<void>;
|
|
20
|
-
getToken: () => Promise<string | null>;
|
|
21
|
-
getTokenInfo: () => Promise<{
|
|
22
|
-
token: string;
|
|
23
|
-
expiresAt: number;
|
|
24
|
-
expiresIn: number;
|
|
25
|
-
} | null>;
|
|
26
|
-
refreshToken: () => Promise<string>;
|
|
27
|
-
getAccount: (forceRefresh?: boolean) => Promise<Record<string, any>>;
|
|
28
|
-
refreshAccount: () => Promise<Record<string, any>>;
|
|
29
|
-
clearAccountCache: () => Promise<void>;
|
|
30
|
-
onAuthStateChange: (callback: AuthStateChangeCallback) => () => void;
|
|
31
|
-
retryVerification: () => Promise<boolean>;
|
|
32
|
-
}
|
|
2
|
+
import type { AuthProviderProps, AuthContextValue } from '../types';
|
|
33
3
|
export declare const AuthContext: React.Context<AuthContextValue | undefined>;
|
|
34
4
|
export type { AuthContextValue };
|
|
35
5
|
export declare const AuthProvider: React.FC<AuthProviderProps>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuthContext.d.ts","sourceRoot":"","sources":["../../src/context/AuthContext.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA8E,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"AuthContext.d.ts","sourceRoot":"","sources":["../../src/context/AuthContext.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA8E,MAAM,OAAO,CAAC;AAMnG,OAAO,KAAK,EAAqC,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAGvG,eAAO,MAAM,WAAW,6CAAyD,CAAC;AAGlF,YAAY,EAAE,gBAAgB,EAAE,CAAC;AAEjC,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAq0BpD,CAAC;AAEF,eAAO,MAAM,OAAO,QAAO,gBAM1B,CAAC"}
|
package/dist/index.esm.js
CHANGED
|
@@ -11530,21 +11530,48 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
11530
11530
|
return null;
|
|
11531
11531
|
}
|
|
11532
11532
|
}, [collectionId, shouldSyncContacts, proxyMode, token, accountData, accountInfo, isVerified, notifyAuthStateChange]);
|
|
11533
|
+
// Detect legacy vs new interactionConfig shape
|
|
11534
|
+
const isLegacyConfig = interactionConfig && !('interactionId' in interactionConfig) && ('login' in interactionConfig || 'logout' in interactionConfig || 'signup' in interactionConfig);
|
|
11535
|
+
if (isLegacyConfig) {
|
|
11536
|
+
console.warn('[AuthContext] Deprecated: interactionConfig uses legacy per-event-type IDs. Migrate to { interactionId, scopes? } shape. Legacy support will be removed in a future release.');
|
|
11537
|
+
}
|
|
11533
11538
|
// Track interaction event (non-blocking)
|
|
11534
|
-
//
|
|
11535
|
-
// Interaction IDs must be pre-created in SmartLinks - you cannot use arbitrary IDs.
|
|
11539
|
+
// Uses scope-based architecture: single interactionId + scope field to differentiate events.
|
|
11536
11540
|
const trackInteraction = useCallback(async (eventType, userId, currentContactId, metadata) => {
|
|
11537
11541
|
if (!collectionId || !shouldTrackInteractions)
|
|
11538
11542
|
return;
|
|
11539
|
-
|
|
11540
|
-
|
|
11541
|
-
|
|
11542
|
-
|
|
11543
|
-
|
|
11544
|
-
|
|
11545
|
-
|
|
11546
|
-
|
|
11543
|
+
let interactionId;
|
|
11544
|
+
let scope;
|
|
11545
|
+
if (isLegacyConfig) {
|
|
11546
|
+
// Legacy: per-event-type IDs (deprecated)
|
|
11547
|
+
const legacyConfig = interactionConfig;
|
|
11548
|
+
const legacyMap = {
|
|
11549
|
+
login: legacyConfig?.login,
|
|
11550
|
+
logout: legacyConfig?.logout,
|
|
11551
|
+
signup: legacyConfig?.signup,
|
|
11552
|
+
session_restore: legacyConfig?.sessionRestore,
|
|
11553
|
+
};
|
|
11554
|
+
interactionId = legacyMap[eventType];
|
|
11555
|
+
if (!interactionId)
|
|
11556
|
+
return;
|
|
11557
|
+
}
|
|
11558
|
+
else if (interactionConfig && 'interactionId' in interactionConfig) {
|
|
11559
|
+
// New: single ID + scope
|
|
11560
|
+
interactionId = interactionConfig.interactionId;
|
|
11561
|
+
if (!interactionId)
|
|
11562
|
+
return;
|
|
11563
|
+
const defaultScopes = {
|
|
11564
|
+
login: 'login',
|
|
11565
|
+
logout: 'logout',
|
|
11566
|
+
signup: 'signup',
|
|
11567
|
+
session_restore: 'session_restore',
|
|
11568
|
+
};
|
|
11569
|
+
const customScopes = interactionConfig.scopes || {};
|
|
11570
|
+
scope = customScopes[eventType === 'session_restore' ? 'sessionRestore' : eventType] || defaultScopes[eventType];
|
|
11571
|
+
}
|
|
11572
|
+
else {
|
|
11547
11573
|
return;
|
|
11574
|
+
}
|
|
11548
11575
|
try {
|
|
11549
11576
|
await smartlinks.interactions.submitPublicEvent(collectionId, {
|
|
11550
11577
|
collectionId,
|
|
@@ -11552,7 +11579,7 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
11552
11579
|
userId,
|
|
11553
11580
|
contactId: currentContactId || undefined,
|
|
11554
11581
|
appId: interactionAppId,
|
|
11555
|
-
|
|
11582
|
+
scope,
|
|
11556
11583
|
outcome: 'completed',
|
|
11557
11584
|
metadata: {
|
|
11558
11585
|
...metadata,
|
|
@@ -11565,7 +11592,7 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
11565
11592
|
catch (err) {
|
|
11566
11593
|
// Non-blocking
|
|
11567
11594
|
}
|
|
11568
|
-
}, [collectionId, shouldTrackInteractions, interactionAppId, interactionConfig, user, token, accountData, accountInfo, isVerified, contact, contactId, notifyAuthStateChange]);
|
|
11595
|
+
}, [collectionId, shouldTrackInteractions, interactionAppId, interactionConfig, isLegacyConfig, user, token, accountData, accountInfo, isVerified, contact, contactId, notifyAuthStateChange]);
|
|
11569
11596
|
// Keep refs in sync with latest callbacks (avoids stale closures)
|
|
11570
11597
|
useEffect(() => {
|
|
11571
11598
|
syncContactRef.current = syncContact;
|
|
@@ -11670,7 +11697,7 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
11670
11697
|
pendingVerificationRef.current = false;
|
|
11671
11698
|
notifyAuthStateChange('SESSION_VERIFIED', storedUser, storedToken.token, storedAccountData || null, null, true, null, storedContactId);
|
|
11672
11699
|
// Track session restore interaction (optional) - use ref for stable dependency
|
|
11673
|
-
if (interactionConfig?.sessionRestore) {
|
|
11700
|
+
if (interactionConfig && ('interactionId' in interactionConfig || interactionConfig?.sessionRestore)) {
|
|
11674
11701
|
trackInteractionRef.current?.('session_restore', storedUser.uid, storedContactId);
|
|
11675
11702
|
}
|
|
11676
11703
|
}
|
|
@@ -11716,7 +11743,7 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
11716
11743
|
return () => {
|
|
11717
11744
|
isMounted = false;
|
|
11718
11745
|
};
|
|
11719
|
-
}, [proxyMode, notifyAuthStateChange, isNetworkError, interactionConfig
|
|
11746
|
+
}, [proxyMode, notifyAuthStateChange, isNetworkError, interactionConfig]);
|
|
11720
11747
|
// Listen for parent auth state changes (proxy mode only)
|
|
11721
11748
|
useEffect(() => {
|
|
11722
11749
|
if (!proxyMode)
|
|
@@ -12085,9 +12112,9 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
12085
12112
|
if (!storedToken?.expiresAt)
|
|
12086
12113
|
return;
|
|
12087
12114
|
const now = Date.now();
|
|
12088
|
-
const
|
|
12089
|
-
const
|
|
12090
|
-
const percentUsed = (
|
|
12115
|
+
const remainingMs = storedToken.expiresAt - now;
|
|
12116
|
+
const totalLifetimeMs = 7 * 24 * 60 * 60 * 1000;
|
|
12117
|
+
const percentUsed = totalLifetimeMs > 0 ? ((totalLifetimeMs - remainingMs) / totalLifetimeMs) * 100 : 100;
|
|
12091
12118
|
if (percentUsed >= refreshThresholdPercent) {
|
|
12092
12119
|
try {
|
|
12093
12120
|
await refreshToken();
|
|
@@ -12283,6 +12310,25 @@ const getExpirationFromResponse = (response) => {
|
|
|
12283
12310
|
};
|
|
12284
12311
|
// Default Smartlinks Google OAuth Client ID (public - safe to expose)
|
|
12285
12312
|
const DEFAULT_GOOGLE_CLIENT_ID = '696509063554-jdlbjl8vsjt7cr0vgkjkjf3ffnvi3a70.apps.googleusercontent.com';
|
|
12313
|
+
// Default Google OAuth proxy URL (hosted on our whitelisted domain)
|
|
12314
|
+
const DEFAULT_GOOGLE_PROXY_URL = 'https://smartlinks-auth-kit.lovable.app/google-proxy.html';
|
|
12315
|
+
// Exact hostnames where Google OAuth is registered and inline/OneTap flow works directly.
|
|
12316
|
+
// Only specific registered origins — NOT broad wildcards like *.lovable.app
|
|
12317
|
+
const WHITELISTED_GOOGLE_OAUTH_HOSTS = [
|
|
12318
|
+
'smartlinks-auth-kit.lovable.app', // This app's dev/preview domain (registered in Google Console)
|
|
12319
|
+
'smartlinks.app', // Production root
|
|
12320
|
+
'localhost', // Local dev
|
|
12321
|
+
'127.0.0.1', // Local dev
|
|
12322
|
+
];
|
|
12323
|
+
/**
|
|
12324
|
+
* Check if the current domain is whitelisted for direct Google OAuth.
|
|
12325
|
+
* Uses exact hostname match (plus subdomain match for smartlinks.app production).
|
|
12326
|
+
* Returns true if OneTap/inline flow can work without a proxy.
|
|
12327
|
+
*/
|
|
12328
|
+
const isWhitelistedGoogleDomain = () => {
|
|
12329
|
+
const hostname = window.location.hostname;
|
|
12330
|
+
return WHITELISTED_GOOGLE_OAUTH_HOSTS.some(domain => hostname === domain || hostname.endsWith(`.${domain}`));
|
|
12331
|
+
};
|
|
12286
12332
|
// Default auth UI configuration when no clientId is provided
|
|
12287
12333
|
const DEFAULT_AUTH_CONFIG = {
|
|
12288
12334
|
branding: {
|
|
@@ -13051,16 +13097,29 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13051
13097
|
}
|
|
13052
13098
|
};
|
|
13053
13099
|
const handleGoogleLogin = async () => {
|
|
13100
|
+
const hasCustomGoogleClientId = !!config?.googleClientId;
|
|
13054
13101
|
const googleClientId = config?.googleClientId || DEFAULT_GOOGLE_CLIENT_ID;
|
|
13055
13102
|
const configuredFlow = config?.googleOAuthFlow || 'oneTap';
|
|
13056
13103
|
const isWebView = detectWebView();
|
|
13057
13104
|
const nativeBridge = getNativeBridge();
|
|
13058
13105
|
const oauthFlow = (configuredFlow === 'oneTap' && isWebView && !nativeBridge) ? 'redirect' : configuredFlow;
|
|
13106
|
+
// Auto-detect proxy need:
|
|
13107
|
+
// - If explicitly configured, use that URL
|
|
13108
|
+
// - If user has their own Google Client ID, they've registered their domains — no proxy needed
|
|
13109
|
+
// - If on a whitelisted SmartLinks domain, inline flow works directly
|
|
13110
|
+
// - Otherwise, auto-use the default proxy URL
|
|
13111
|
+
const isWhitelisted = isWhitelistedGoogleDomain();
|
|
13112
|
+
const googleProxyUrl = config?.googleOAuthProxyUrl
|
|
13113
|
+
|| (!hasCustomGoogleClientId && !isWhitelisted ? DEFAULT_GOOGLE_PROXY_URL : undefined);
|
|
13059
13114
|
log.log('Google Auth initiated:', {
|
|
13060
13115
|
configuredFlow,
|
|
13061
13116
|
effectiveFlow: oauthFlow,
|
|
13062
13117
|
isWebView,
|
|
13063
13118
|
hasNativeBridge: !!nativeBridge,
|
|
13119
|
+
hasCustomGoogleClientId,
|
|
13120
|
+
isWhitelistedDomain: isWhitelisted,
|
|
13121
|
+
usingProxy: !!googleProxyUrl,
|
|
13122
|
+
proxyUrl: googleProxyUrl,
|
|
13064
13123
|
});
|
|
13065
13124
|
setLoading(true);
|
|
13066
13125
|
setError(undefined);
|
|
@@ -13135,6 +13194,70 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13135
13194
|
setLoading(false);
|
|
13136
13195
|
return;
|
|
13137
13196
|
}
|
|
13197
|
+
// Priority 3: Google OAuth Proxy popup (for custom domains not registered in Google Console)
|
|
13198
|
+
if (googleProxyUrl) {
|
|
13199
|
+
log.log('Using Google OAuth proxy popup:', googleProxyUrl);
|
|
13200
|
+
const proxyParams = new URLSearchParams({
|
|
13201
|
+
clientId,
|
|
13202
|
+
returnOrigin: window.location.origin,
|
|
13203
|
+
...(apiEndpoint ? { apiEndpoint } : {}),
|
|
13204
|
+
});
|
|
13205
|
+
const popupUrl = `${googleProxyUrl}?${proxyParams.toString()}`;
|
|
13206
|
+
const popup = window.open(popupUrl, 'google-oauth-proxy', 'width=500,height=600,menubar=no,toolbar=no,location=yes');
|
|
13207
|
+
if (!popup) {
|
|
13208
|
+
setError('Popup blocked. Please allow popups for this site and try again.');
|
|
13209
|
+
setLoading(false);
|
|
13210
|
+
return;
|
|
13211
|
+
}
|
|
13212
|
+
// Listen for result from proxy popup
|
|
13213
|
+
const handleProxyMessage = async (event) => {
|
|
13214
|
+
// Validate origin matches proxy URL
|
|
13215
|
+
try {
|
|
13216
|
+
const proxyOrigin = new URL(googleProxyUrl).origin;
|
|
13217
|
+
if (event.origin !== proxyOrigin)
|
|
13218
|
+
return;
|
|
13219
|
+
}
|
|
13220
|
+
catch {
|
|
13221
|
+
return;
|
|
13222
|
+
}
|
|
13223
|
+
if (event.data?.type !== 'smartlinks:google-proxy:result')
|
|
13224
|
+
return;
|
|
13225
|
+
window.removeEventListener('message', handleProxyMessage);
|
|
13226
|
+
clearInterval(checkClosed);
|
|
13227
|
+
try {
|
|
13228
|
+
if (event.data.success && event.data.token) {
|
|
13229
|
+
const { token, user, accountData, isNewUser, expiresAt, expiresIn } = event.data;
|
|
13230
|
+
const expiration = expiresAt || (expiresIn ? Date.now() + expiresIn : undefined);
|
|
13231
|
+
await auth.login(token, user, accountData, isNewUser, expiration);
|
|
13232
|
+
setAuthSuccess(true);
|
|
13233
|
+
setSuccessMessage('Google login successful!');
|
|
13234
|
+
onAuthSuccess(token, user, accountData);
|
|
13235
|
+
}
|
|
13236
|
+
else {
|
|
13237
|
+
const errorMsg = event.data.error || 'Google login failed';
|
|
13238
|
+
setError(getFriendlyErrorMessage(errorMsg));
|
|
13239
|
+
onAuthError?.(new Error(errorMsg));
|
|
13240
|
+
}
|
|
13241
|
+
}
|
|
13242
|
+
catch (err) {
|
|
13243
|
+
setError(getFriendlyErrorMessage(err));
|
|
13244
|
+
onAuthError?.(err instanceof Error ? err : new Error(getFriendlyErrorMessage(err)));
|
|
13245
|
+
}
|
|
13246
|
+
finally {
|
|
13247
|
+
setLoading(false);
|
|
13248
|
+
}
|
|
13249
|
+
};
|
|
13250
|
+
window.addEventListener('message', handleProxyMessage);
|
|
13251
|
+
// Monitor popup close without result (user closed it)
|
|
13252
|
+
const checkClosed = setInterval(() => {
|
|
13253
|
+
if (popup.closed) {
|
|
13254
|
+
clearInterval(checkClosed);
|
|
13255
|
+
window.removeEventListener('message', handleProxyMessage);
|
|
13256
|
+
setLoading(false);
|
|
13257
|
+
}
|
|
13258
|
+
}, 500);
|
|
13259
|
+
return; // Don't set loading to false - waiting for popup
|
|
13260
|
+
}
|
|
13138
13261
|
log.log('Using web-based OAuth flow:', oauthFlow);
|
|
13139
13262
|
// Priority 3: Web-based flows (redirect, popup, oneTap)
|
|
13140
13263
|
// Dynamically load Google Identity Services if not already loaded
|
|
@@ -13368,7 +13491,6 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13368
13491
|
if (resetToken && confirmPassword) {
|
|
13369
13492
|
// Complete password reset with token
|
|
13370
13493
|
await api.completePasswordReset(resetToken, emailOrPassword);
|
|
13371
|
-
await api.completePasswordReset(resetToken, emailOrPassword);
|
|
13372
13494
|
// Auto-login with the new password if we have the email
|
|
13373
13495
|
if (resetEmail) {
|
|
13374
13496
|
try {
|
|
@@ -13693,6 +13815,12 @@ const AccountManagement = ({ apiEndpoint, clientId, collectionId, onError, class
|
|
|
13693
13815
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
|
13694
13816
|
const { showProfileSection = true, showEmailSection = true, showPasswordSection = true, showPhoneSection = true, showDeleteAccount = false, showCustomFields = true, // New: show schema-driven custom fields
|
|
13695
13817
|
} = customization;
|
|
13818
|
+
// Provider awareness helpers
|
|
13819
|
+
// If providers is undefined (backend not updated yet), fall back to showing everything
|
|
13820
|
+
const providers = profile?.providers;
|
|
13821
|
+
const hasPassword = profile?.hasPassword ?? (providers === undefined ? undefined : providers?.includes('password'));
|
|
13822
|
+
const isGoogleOnly = providers !== undefined && providers.includes('google.com') && !providers.includes('password');
|
|
13823
|
+
const isPhoneOnly = providers !== undefined && providers.includes('phone') && providers.length === 1;
|
|
13696
13824
|
// Reinitialize Smartlinks SDK when apiEndpoint changes
|
|
13697
13825
|
useEffect(() => {
|
|
13698
13826
|
if (apiEndpoint) {
|
|
@@ -13710,13 +13838,11 @@ const AccountManagement = ({ apiEndpoint, clientId, collectionId, onError, class
|
|
|
13710
13838
|
const loadSchema = async () => {
|
|
13711
13839
|
setSchemaLoading(true);
|
|
13712
13840
|
try {
|
|
13713
|
-
console.log('[AccountManagement] Loading schema for collection:', collectionId);
|
|
13714
13841
|
const schemaResult = await smartlinks.contact.publicGetSchema(collectionId);
|
|
13715
|
-
console.log('[AccountManagement] Schema loaded:', schemaResult);
|
|
13716
13842
|
setSchema(schemaResult);
|
|
13717
13843
|
}
|
|
13718
13844
|
catch (err) {
|
|
13719
|
-
|
|
13845
|
+
// Non-fatal - component works without schema
|
|
13720
13846
|
// Non-fatal - component works without schema
|
|
13721
13847
|
}
|
|
13722
13848
|
finally {
|
|
@@ -13731,7 +13857,6 @@ const AccountManagement = ({ apiEndpoint, clientId, collectionId, onError, class
|
|
|
13731
13857
|
// where getContact() is called before the server knows about the user.
|
|
13732
13858
|
useEffect(() => {
|
|
13733
13859
|
if (!auth.isAuthenticated || !auth.isVerified) {
|
|
13734
|
-
console.log('[AccountManagement] Waiting for verified authentication before loading profile...');
|
|
13735
13860
|
return;
|
|
13736
13861
|
}
|
|
13737
13862
|
loadProfile();
|
|
@@ -13832,7 +13957,7 @@ const AccountManagement = ({ apiEndpoint, clientId, collectionId, onError, class
|
|
|
13832
13957
|
setError(undefined);
|
|
13833
13958
|
setSuccess(undefined);
|
|
13834
13959
|
try {
|
|
13835
|
-
|
|
13960
|
+
await auth.updateContactCustomFields?.(customFieldValues);
|
|
13836
13961
|
await auth.updateContactCustomFields?.(customFieldValues);
|
|
13837
13962
|
setSuccess('Profile updated successfully!');
|
|
13838
13963
|
setEditingSection(null);
|
|
@@ -13958,7 +14083,8 @@ const AccountManagement = ({ apiEndpoint, clientId, collectionId, onError, class
|
|
|
13958
14083
|
setError('Please type DELETE to confirm account deletion');
|
|
13959
14084
|
return;
|
|
13960
14085
|
}
|
|
13961
|
-
|
|
14086
|
+
// Only require password for users who have one
|
|
14087
|
+
if (hasPassword !== false && !deletePassword) {
|
|
13962
14088
|
setError('Password is required');
|
|
13963
14089
|
return;
|
|
13964
14090
|
}
|
|
@@ -13988,7 +14114,7 @@ const AccountManagement = ({ apiEndpoint, clientId, collectionId, onError, class
|
|
|
13988
14114
|
}
|
|
13989
14115
|
return (jsxs("div", { className: `account-management ${className}`, style: { maxWidth: '600px' }, children: [error && (jsx("div", { className: "auth-error", role: "alert", children: error })), success && (jsx("div", { className: "auth-success", role: "alert", children: success })), showProfileSection && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Display Name" }), jsx("div", { className: "field-value", children: profile?.displayName || 'Not set' })] }), editingSection !== 'profile' && (jsx("button", { type: "button", onClick: () => setEditingSection('profile'), className: "auth-button button-secondary", children: "Change" }))] }), editingSection === 'profile' && (jsxs("form", { onSubmit: handleUpdateProfile, className: "edit-form", children: [jsx("div", { className: "form-group", children: jsx("input", { id: "displayName", type: "text", value: displayName, onChange: (e) => setDisplayName(e.target.value), placeholder: "Your display name", className: "auth-input" }) }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Saving...' : 'Save' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showCustomFields && collectionId && editableCustomFields.length > 0 && (jsxs("section", { className: "account-section", children: [jsx("h3", { className: "section-title", style: { fontSize: '0.9rem', fontWeight: 600, marginBottom: '12px' }, children: "Additional Information" }), editingSection !== 'customFields' ? (jsxs(Fragment, { children: [editableCustomFields.map((field) => (jsx("div", { className: "account-field", children: jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: field.label }), jsx("div", { className: "field-value", children: customFieldValues[field.key] !== undefined && customFieldValues[field.key] !== ''
|
|
13990
14116
|
? String(customFieldValues[field.key])
|
|
13991
|
-
: 'Not set' })] }) }, field.key))), jsx("button", { type: "button", onClick: () => setEditingSection('customFields'), className: "auth-button button-secondary", style: { marginTop: '8px' }, children: "Edit Information" })] })) : (jsxs("form", { onSubmit: handleUpdateCustomFields, className: "edit-form", children: [editableCustomFields.map((field) => (jsx(SchemaFieldRenderer, { field: field, value: customFieldValues[field.key], onChange: handleCustomFieldChange, disabled: loading }, field.key))), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Saving...' : 'Save' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showEmailSection && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Email Address" }), jsxs("div", { className: "field-value", children: [profile?.email || 'Not set', profile?.emailVerified && (jsx("span", { className: "verification-badge verified", children: "Verified" })), profile?.email && !profile?.emailVerified && (jsx("span", { className: "verification-badge unverified", children: "Unverified" }))] })] }), editingSection !== 'email' && (jsx("button", { type: "button", onClick: () => setEditingSection('email'), className: "auth-button button-secondary", children: "Change Email" }))] }), editingSection === 'email' && (jsxs("form", { onSubmit: handleChangeEmail, className: "edit-form", children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "newEmail", children: "New Email" }), jsx("input", { id: "newEmail", type: "email", value: newEmail, onChange: (e) => setNewEmail(e.target.value), placeholder: "new.email@example.com", className: "auth-input", required: true })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "emailPassword", children: "Confirm Password" }), jsx("input", { id: "emailPassword", type: "password", value: emailPassword, onChange: (e) => setEmailPassword(e.target.value), placeholder: "Enter your password", className: "auth-input", required: true })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Changing...' : 'Change Email' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showPasswordSection && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Password" }), jsx("div", { className: "field-value", children: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" })] }), editingSection !== 'password' && (jsx("button", { type: "button", onClick: () => setEditingSection('password'), className: "auth-button button-secondary", children: "Change Password" }))] }), editingSection === 'password' && (jsxs("form", { onSubmit: handleChangePassword, className: "edit-form", children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "currentPassword", children: "Current Password" }), jsx("input", { id: "currentPassword", type: "password", value: currentPassword, onChange: (e) => setCurrentPassword(e.target.value), placeholder: "Enter current password", className: "auth-input", required: true })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "newPassword", children: "New Password" }), jsx("input", { id: "newPassword", type: "password", value: newPassword, onChange: (e) => setNewPassword(e.target.value), placeholder: "Enter new password", className: "auth-input", required: true, minLength: 6 })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "confirmPassword", children: "Confirm New Password" }), jsx("input", { id: "confirmPassword", type: "password", value: confirmPassword, onChange: (e) => setConfirmPassword(e.target.value), placeholder: "Confirm new password", className: "auth-input", required: true, minLength: 6 })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Changing...' : 'Change Password' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showPhoneSection && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Phone Number" }), jsx("div", { className: "field-value", children: profile?.phoneNumber || 'Not set' })] }), editingSection !== 'phone' && (jsx("button", { type: "button", onClick: () => setEditingSection('phone'), className: "auth-button button-secondary", children: "Change Phone" }))] }), editingSection === 'phone' && (jsxs("form", { onSubmit: handleUpdatePhone, className: "edit-form", children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "newPhone", children: "New Phone Number" }), jsx("input", { id: "newPhone", type: "tel", value: newPhone, onChange: (e) => setNewPhone(e.target.value), placeholder: "+1234567890", className: "auth-input", required: true })] }), !phoneCodeSent ? (jsxs("div", { className: "button-group", children: [jsx("button", { type: "button", onClick: handleSendPhoneCode, className: "auth-button", disabled: loading || !newPhone, children: loading ? 'Sending...' : 'Send Code' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })) : (jsxs(Fragment, { children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "phoneCode", children: "Verification Code" }), jsx("input", { id: "phoneCode", type: "text", value: phoneCode, onChange: (e) => setPhoneCode(e.target.value), placeholder: "Enter 6-digit code", className: "auth-input", required: true, maxLength: 6 })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Verifying...' : 'Verify & Save' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] }))] })), showDeleteAccount && (jsxs("section", { className: "account-section danger-zone", children: [jsx("h3", { className: "section-title text-danger", children: "Danger Zone" }), !showDeleteConfirm ? (jsx("button", { type: "button", onClick: () => setShowDeleteConfirm(true), className: "auth-button button-danger", children: "Delete Account" })) : (jsxs("div", { className: "delete-confirm", children: [jsx("p", { className: "warning-text", children: "\u26A0\uFE0F This action cannot be undone. This will permanently delete your account and all associated data." }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "deletePassword", children: "Confirm Password" }), jsx("input", { id: "deletePassword", type: "password", value: deletePassword, onChange: (e) => setDeletePassword(e.target.value), placeholder: "Enter your password", className: "auth-input" })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "deleteConfirm", children: "Type DELETE to confirm" }), jsx("input", { id: "deleteConfirm", type: "text", value: deleteConfirmText, onChange: (e) => setDeleteConfirmText(e.target.value), placeholder: "DELETE", className: "auth-input" })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "button", onClick: handleDeleteAccount, className: "auth-button button-danger", disabled: loading, children: loading ? 'Deleting...' : 'Permanently Delete Account' }), jsx("button", { type: "button", onClick: () => {
|
|
14117
|
+
: 'Not set' })] }) }, field.key))), jsx("button", { type: "button", onClick: () => setEditingSection('customFields'), className: "auth-button button-secondary", style: { marginTop: '8px' }, children: "Edit Information" })] })) : (jsxs("form", { onSubmit: handleUpdateCustomFields, className: "edit-form", children: [editableCustomFields.map((field) => (jsx(SchemaFieldRenderer, { field: field, value: customFieldValues[field.key], onChange: handleCustomFieldChange, disabled: loading }, field.key))), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Saving...' : 'Save' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showEmailSection && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Email Address" }), jsxs("div", { className: "field-value", children: [profile?.email || 'Not set', profile?.emailVerified && (jsx("span", { className: "verification-badge verified", children: "Verified" })), profile?.email && !profile?.emailVerified && (jsx("span", { className: "verification-badge unverified", children: "Unverified" }))] }), isGoogleOnly && (jsx("div", { className: "field-hint", style: { fontSize: '0.8rem', color: 'var(--color-muted-foreground, #888)', marginTop: '4px' }, children: "This email is managed by your Google account" }))] }), editingSection !== 'email' && !isGoogleOnly && !isPhoneOnly && (jsx("button", { type: "button", onClick: () => setEditingSection('email'), className: "auth-button button-secondary", children: "Change Email" }))] }), editingSection === 'email' && !isGoogleOnly && !isPhoneOnly && (jsxs("form", { onSubmit: handleChangeEmail, className: "edit-form", children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "newEmail", children: "New Email" }), jsx("input", { id: "newEmail", type: "email", value: newEmail, onChange: (e) => setNewEmail(e.target.value), placeholder: "new.email@example.com", className: "auth-input", required: true })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "emailPassword", children: "Confirm Password" }), jsx("input", { id: "emailPassword", type: "password", value: emailPassword, onChange: (e) => setEmailPassword(e.target.value), placeholder: "Enter your password", className: "auth-input", required: true })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Changing...' : 'Change Email' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showPasswordSection && !isGoogleOnly && !isPhoneOnly && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Password" }), jsx("div", { className: "field-value", children: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" })] }), editingSection !== 'password' && (jsx("button", { type: "button", onClick: () => setEditingSection('password'), className: "auth-button button-secondary", children: "Change Password" }))] }), editingSection === 'password' && (jsxs("form", { onSubmit: handleChangePassword, className: "edit-form", children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "currentPassword", children: "Current Password" }), jsx("input", { id: "currentPassword", type: "password", value: currentPassword, onChange: (e) => setCurrentPassword(e.target.value), placeholder: "Enter current password", className: "auth-input", required: true })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "newPassword", children: "New Password" }), jsx("input", { id: "newPassword", type: "password", value: newPassword, onChange: (e) => setNewPassword(e.target.value), placeholder: "Enter new password", className: "auth-input", required: true, minLength: 6 })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "confirmPassword", children: "Confirm New Password" }), jsx("input", { id: "confirmPassword", type: "password", value: confirmPassword, onChange: (e) => setConfirmPassword(e.target.value), placeholder: "Confirm new password", className: "auth-input", required: true, minLength: 6 })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Changing...' : 'Change Password' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showPhoneSection && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Phone Number" }), jsx("div", { className: "field-value", children: profile?.phoneNumber || 'Not set' })] }), editingSection !== 'phone' && (jsx("button", { type: "button", onClick: () => setEditingSection('phone'), className: "auth-button button-secondary", children: "Change Phone" }))] }), editingSection === 'phone' && (jsxs("form", { onSubmit: handleUpdatePhone, className: "edit-form", children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "newPhone", children: "New Phone Number" }), jsx("input", { id: "newPhone", type: "tel", value: newPhone, onChange: (e) => setNewPhone(e.target.value), placeholder: "+1234567890", className: "auth-input", required: true })] }), !phoneCodeSent ? (jsxs("div", { className: "button-group", children: [jsx("button", { type: "button", onClick: handleSendPhoneCode, className: "auth-button", disabled: loading || !newPhone, children: loading ? 'Sending...' : 'Send Code' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })) : (jsxs(Fragment, { children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "phoneCode", children: "Verification Code" }), jsx("input", { id: "phoneCode", type: "text", value: phoneCode, onChange: (e) => setPhoneCode(e.target.value), placeholder: "Enter 6-digit code", className: "auth-input", required: true, maxLength: 6 })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Verifying...' : 'Verify & Save' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] }))] })), showDeleteAccount && (jsxs("section", { className: "account-section danger-zone", children: [jsx("h3", { className: "section-title text-danger", children: "Danger Zone" }), !showDeleteConfirm ? (jsx("button", { type: "button", onClick: () => setShowDeleteConfirm(true), className: "auth-button button-danger", children: "Delete Account" })) : (jsxs("div", { className: "delete-confirm", children: [jsx("p", { className: "warning-text", children: "\u26A0\uFE0F This action cannot be undone. This will permanently delete your account and all associated data." }), hasPassword !== false && (jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "deletePassword", children: "Confirm Password" }), jsx("input", { id: "deletePassword", type: "password", value: deletePassword, onChange: (e) => setDeletePassword(e.target.value), placeholder: "Enter your password", className: "auth-input" })] })), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "deleteConfirm", children: "Type DELETE to confirm" }), jsx("input", { id: "deleteConfirm", type: "text", value: deleteConfirmText, onChange: (e) => setDeleteConfirmText(e.target.value), placeholder: "DELETE", className: "auth-input" })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "button", onClick: handleDeleteAccount, className: "auth-button button-danger", disabled: loading, children: loading ? 'Deleting...' : 'Permanently Delete Account' }), jsx("button", { type: "button", onClick: () => {
|
|
13992
14118
|
setShowDeleteConfirm(false);
|
|
13993
14119
|
setDeletePassword('');
|
|
13994
14120
|
setDeleteConfirmText('');
|