@proveanything/smartlinks-auth-ui 0.1.3 → 0.1.5
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/README.md +385 -228
- package/dist/components/AccountManagement.d.ts.map +1 -1
- package/dist/components/AuthContainer.d.ts +1 -0
- package/dist/components/AuthContainer.d.ts.map +1 -1
- package/dist/components/AuthUIPreview.d.ts +1 -0
- package/dist/components/AuthUIPreview.d.ts.map +1 -1
- package/dist/components/ClaimUI.d.ts +4 -0
- package/dist/components/ClaimUI.d.ts.map +1 -0
- package/dist/components/EmailAuthForm.d.ts +2 -1
- package/dist/components/EmailAuthForm.d.ts.map +1 -1
- package/dist/components/SmartlinksAuthUI.d.ts +4 -0
- package/dist/components/SmartlinksAuthUI.d.ts.map +1 -0
- package/dist/context/AuthContext.d.ts +10 -3
- package/dist/context/AuthContext.d.ts.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +3 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.css +1 -1
- package/dist/index.esm.css.map +1 -1
- package/dist/index.esm.js +1602 -914
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1602 -913
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +66 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/persistentStorage.d.ts +39 -0
- package/dist/utils/persistentStorage.d.ts.map +1 -0
- package/dist/utils/tokenStorage.d.ts +23 -10
- package/dist/utils/tokenStorage.d.ts.map +1 -1
- package/package.json +2 -1
package/dist/index.esm.js
CHANGED
|
@@ -2,7 +2,7 @@ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
|
2
2
|
import React, { useEffect, useState, useRef, useCallback, useMemo, createContext, useContext } from 'react';
|
|
3
3
|
import * as smartlinks from '@proveanything/smartlinks';
|
|
4
4
|
|
|
5
|
-
const AuthContainer = ({ children, theme = 'light', className = '', config, }) => {
|
|
5
|
+
const AuthContainer = ({ children, theme = 'light', className = '', config, minimal = false, }) => {
|
|
6
6
|
// Apply CSS variables for customization
|
|
7
7
|
useEffect(() => {
|
|
8
8
|
if (!config?.branding)
|
|
@@ -42,11 +42,21 @@ const AuthContainer = ({ children, theme = 'light', className = '', config, }) =
|
|
|
42
42
|
}, [config]);
|
|
43
43
|
const title = config?.branding?.title;
|
|
44
44
|
const subtitle = config?.branding?.subtitle;
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
// Logo URL logic:
|
|
46
|
+
// - undefined/not set: use default Smartlinks logo
|
|
47
|
+
// - null or empty string: hide logo completely
|
|
48
|
+
// - string URL: use custom logo
|
|
49
|
+
const logoUrl = config?.branding?.logoUrl === undefined
|
|
50
|
+
? 'https://smartlinks.app/smartlinks/landscape-medium.png' // Default Smartlinks logo
|
|
51
|
+
: config?.branding?.logoUrl || null; // Custom or explicitly hidden
|
|
52
|
+
const containerClass = minimal
|
|
53
|
+
? `auth-minimal auth-theme-${theme} ${className}`
|
|
54
|
+
: `auth-container auth-theme-${theme} ${className}`;
|
|
55
|
+
const cardClass = minimal ? 'auth-minimal-card' : 'auth-card';
|
|
56
|
+
return (jsx("div", { className: containerClass, children: jsxs("div", { className: cardClass, style: !minimal && config?.branding?.buttonStyle === 'square' ? { borderRadius: '4px' } : undefined, children: [(logoUrl || title || subtitle) && (jsxs("div", { className: "auth-header", children: [logoUrl && (jsx("div", { className: "auth-logo", children: jsx("img", { src: logoUrl, alt: "Logo", style: { maxWidth: '200px', height: 'auto', objectFit: 'contain' } }) })), title && jsx("h1", { className: "auth-title", children: title }), subtitle && jsx("p", { className: "auth-subtitle", children: subtitle })] })), jsx("div", { className: "auth-content", children: children }), (config?.branding?.termsUrl || config?.branding?.privacyUrl) && (jsxs("div", { className: "auth-footer", children: [config.branding.termsUrl && jsx("a", { href: config.branding.termsUrl, target: "_blank", rel: "noopener noreferrer", children: "Terms" }), config.branding.termsUrl && config.branding.privacyUrl && jsx("span", { children: "\u2022" }), config.branding.privacyUrl && jsx("a", { href: config.branding.privacyUrl, target: "_blank", rel: "noopener noreferrer", children: "Privacy" })] }))] }) }));
|
|
47
57
|
};
|
|
48
58
|
|
|
49
|
-
const EmailAuthForm = ({ mode, onSubmit, onModeSwitch, onForgotPassword, loading, error, }) => {
|
|
59
|
+
const EmailAuthForm = ({ mode, onSubmit, onModeSwitch, onForgotPassword, loading, error, additionalFields = [], }) => {
|
|
50
60
|
const [formData, setFormData] = useState({
|
|
51
61
|
email: '',
|
|
52
62
|
password: '',
|
|
@@ -61,7 +71,7 @@ const EmailAuthForm = ({ mode, onSubmit, onModeSwitch, onForgotPassword, loading
|
|
|
61
71
|
};
|
|
62
72
|
return (jsxs("form", { className: "auth-form", onSubmit: handleSubmit, children: [jsxs("div", { className: "auth-form-header", children: [jsx("h2", { className: "auth-form-title", children: mode === 'login' ? 'Sign in' : 'Create account' }), jsx("p", { className: "auth-form-subtitle", children: mode === 'login'
|
|
63
73
|
? 'Welcome back! Please enter your credentials.'
|
|
64
|
-
: 'Get started by creating your account.' })] }), 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 Doe" })] })), 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 === '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') }), jsxs("div", { className: "auth-divider", children: [jsx("span", { children: mode === 'login' ? "Don't have an account?" : 'Already have an account?' }), jsx("button", { type: "button", className: "auth-link auth-link-bold", onClick: onModeSwitch, disabled: loading, children: mode === 'login' ? 'Sign up' : 'Sign in' })] })] }));
|
|
74
|
+
: 'Get started by creating your account.' })] }), 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 Doe" })] })), 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' && additionalFields.map((field) => (jsxs("div", { className: "auth-form-group", children: [jsxs("label", { htmlFor: field.name, className: "auth-label", children: [field.label, field.required && jsx("span", { style: { color: 'var(--auth-error-color, #ef4444)' }, children: " *" })] }), field.type === 'select' ? (jsxs("select", { id: field.name, className: "auth-input", value: formData[field.name] || '', onChange: (e) => handleChange(field.name, e.target.value), required: field.required, disabled: loading, children: [jsx("option", { value: "", children: "Select..." }), field.options?.map((option) => (jsx("option", { value: option, children: option }, option)))] })) : field.type === 'textarea' ? (jsx("textarea", { id: field.name, className: "auth-input", value: formData[field.name] || '', onChange: (e) => handleChange(field.name, e.target.value), required: field.required, disabled: loading, placeholder: field.placeholder, rows: 3, style: { minHeight: '80px', resize: 'vertical' } })) : (jsx("input", { type: field.type, id: field.name, className: "auth-input", value: formData[field.name] || '', onChange: (e) => handleChange(field.name, e.target.value), required: field.required, disabled: loading, placeholder: field.placeholder }))] }, field.name))), 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') }), jsxs("div", { className: "auth-divider", children: [jsx("span", { children: mode === 'login' ? "Don't have an account?" : 'Already have an account?' }), jsx("button", { type: "button", className: "auth-link auth-link-bold", onClick: onModeSwitch, disabled: loading, children: mode === 'login' ? 'Sign up' : 'Sign in' })] })] }));
|
|
65
75
|
};
|
|
66
76
|
|
|
67
77
|
const ProviderButtons = ({ enabledProviders, providerOrder, onEmailLogin, onGoogleLogin, onPhoneLogin, onMagicLinkLogin, loading, }) => {
|
|
@@ -10672,142 +10682,724 @@ class AuthAPI {
|
|
|
10672
10682
|
}
|
|
10673
10683
|
}
|
|
10674
10684
|
|
|
10675
|
-
|
|
10676
|
-
|
|
10677
|
-
|
|
10685
|
+
/**
|
|
10686
|
+
* Persistent Storage Layer for Smartlinks Auth
|
|
10687
|
+
*
|
|
10688
|
+
* Implements a tiered persistence strategy similar to Firebase Auth:
|
|
10689
|
+
* 1. Primary: IndexedDB (async, large capacity, cross-tab)
|
|
10690
|
+
* 2. Fallback: localStorage (sync, universal support)
|
|
10691
|
+
*
|
|
10692
|
+
* Provides unified async API for all storage operations with automatic
|
|
10693
|
+
* fallback handling and cross-tab synchronization.
|
|
10694
|
+
*/
|
|
10695
|
+
const DB_NAME = 'smartlinks_auth_db';
|
|
10696
|
+
const DB_VERSION = 1;
|
|
10697
|
+
const STORE_NAME = 'auth_state';
|
|
10698
|
+
const STORAGE_CHANNEL = 'smartlinks_auth_storage';
|
|
10699
|
+
/**
|
|
10700
|
+
* IndexedDB Storage Implementation
|
|
10701
|
+
*/
|
|
10702
|
+
class IndexedDBStorage {
|
|
10703
|
+
constructor() {
|
|
10704
|
+
this.db = null;
|
|
10705
|
+
this.initPromise = null;
|
|
10706
|
+
this.channel = null;
|
|
10707
|
+
this.initPromise = this.initialize();
|
|
10708
|
+
// Set up BroadcastChannel for cross-tab communication
|
|
10709
|
+
if (typeof BroadcastChannel !== 'undefined') {
|
|
10710
|
+
this.channel = new BroadcastChannel(STORAGE_CHANNEL);
|
|
10711
|
+
}
|
|
10712
|
+
}
|
|
10713
|
+
async initialize() {
|
|
10714
|
+
return new Promise((resolve, reject) => {
|
|
10715
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
10716
|
+
request.onerror = () => {
|
|
10717
|
+
console.warn('IndexedDB initialization failed:', request.error);
|
|
10718
|
+
reject(request.error);
|
|
10719
|
+
};
|
|
10720
|
+
request.onsuccess = () => {
|
|
10721
|
+
this.db = request.result;
|
|
10722
|
+
resolve();
|
|
10723
|
+
};
|
|
10724
|
+
request.onupgradeneeded = (event) => {
|
|
10725
|
+
const db = event.target.result;
|
|
10726
|
+
// Create object store if it doesn't exist
|
|
10727
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
10728
|
+
db.createObjectStore(STORE_NAME);
|
|
10729
|
+
}
|
|
10730
|
+
};
|
|
10731
|
+
});
|
|
10732
|
+
}
|
|
10733
|
+
async ensureInitialized() {
|
|
10734
|
+
if (this.initPromise) {
|
|
10735
|
+
await this.initPromise;
|
|
10736
|
+
}
|
|
10737
|
+
}
|
|
10738
|
+
broadcastChange(event) {
|
|
10739
|
+
if (this.channel) {
|
|
10740
|
+
try {
|
|
10741
|
+
this.channel.postMessage(event);
|
|
10742
|
+
}
|
|
10743
|
+
catch (error) {
|
|
10744
|
+
console.warn('Failed to broadcast storage change:', error);
|
|
10745
|
+
}
|
|
10746
|
+
}
|
|
10747
|
+
}
|
|
10748
|
+
async setItem(key, value) {
|
|
10749
|
+
await this.ensureInitialized();
|
|
10750
|
+
return new Promise((resolve, reject) => {
|
|
10751
|
+
if (!this.db) {
|
|
10752
|
+
reject(new Error('IndexedDB not initialized'));
|
|
10753
|
+
return;
|
|
10754
|
+
}
|
|
10755
|
+
const transaction = this.db.transaction([STORE_NAME], 'readwrite');
|
|
10756
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
10757
|
+
const request = store.put(value, key);
|
|
10758
|
+
request.onsuccess = () => {
|
|
10759
|
+
this.broadcastChange({
|
|
10760
|
+
type: 'set',
|
|
10761
|
+
key,
|
|
10762
|
+
value,
|
|
10763
|
+
timestamp: Date.now(),
|
|
10764
|
+
});
|
|
10765
|
+
resolve();
|
|
10766
|
+
};
|
|
10767
|
+
request.onerror = () => {
|
|
10768
|
+
reject(request.error);
|
|
10769
|
+
};
|
|
10770
|
+
});
|
|
10771
|
+
}
|
|
10772
|
+
async getItem(key) {
|
|
10773
|
+
await this.ensureInitialized();
|
|
10774
|
+
return new Promise((resolve, reject) => {
|
|
10775
|
+
if (!this.db) {
|
|
10776
|
+
reject(new Error('IndexedDB not initialized'));
|
|
10777
|
+
return;
|
|
10778
|
+
}
|
|
10779
|
+
const transaction = this.db.transaction([STORE_NAME], 'readonly');
|
|
10780
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
10781
|
+
const request = store.get(key);
|
|
10782
|
+
request.onsuccess = () => {
|
|
10783
|
+
resolve(request.result !== undefined ? request.result : null);
|
|
10784
|
+
};
|
|
10785
|
+
request.onerror = () => {
|
|
10786
|
+
reject(request.error);
|
|
10787
|
+
};
|
|
10788
|
+
});
|
|
10789
|
+
}
|
|
10790
|
+
async removeItem(key) {
|
|
10791
|
+
await this.ensureInitialized();
|
|
10792
|
+
return new Promise((resolve, reject) => {
|
|
10793
|
+
if (!this.db) {
|
|
10794
|
+
reject(new Error('IndexedDB not initialized'));
|
|
10795
|
+
return;
|
|
10796
|
+
}
|
|
10797
|
+
const transaction = this.db.transaction([STORE_NAME], 'readwrite');
|
|
10798
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
10799
|
+
const request = store.delete(key);
|
|
10800
|
+
request.onsuccess = () => {
|
|
10801
|
+
this.broadcastChange({
|
|
10802
|
+
type: 'remove',
|
|
10803
|
+
key,
|
|
10804
|
+
timestamp: Date.now(),
|
|
10805
|
+
});
|
|
10806
|
+
resolve();
|
|
10807
|
+
};
|
|
10808
|
+
request.onerror = () => {
|
|
10809
|
+
reject(request.error);
|
|
10810
|
+
};
|
|
10811
|
+
});
|
|
10812
|
+
}
|
|
10813
|
+
async clear() {
|
|
10814
|
+
await this.ensureInitialized();
|
|
10815
|
+
return new Promise((resolve, reject) => {
|
|
10816
|
+
if (!this.db) {
|
|
10817
|
+
reject(new Error('IndexedDB not initialized'));
|
|
10818
|
+
return;
|
|
10819
|
+
}
|
|
10820
|
+
const transaction = this.db.transaction([STORE_NAME], 'readwrite');
|
|
10821
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
10822
|
+
const request = store.clear();
|
|
10823
|
+
request.onsuccess = () => {
|
|
10824
|
+
this.broadcastChange({
|
|
10825
|
+
type: 'clear',
|
|
10826
|
+
timestamp: Date.now(),
|
|
10827
|
+
});
|
|
10828
|
+
resolve();
|
|
10829
|
+
};
|
|
10830
|
+
request.onerror = () => {
|
|
10831
|
+
reject(request.error);
|
|
10832
|
+
};
|
|
10833
|
+
});
|
|
10834
|
+
}
|
|
10835
|
+
async isAvailable() {
|
|
10836
|
+
try {
|
|
10837
|
+
await this.ensureInitialized();
|
|
10838
|
+
return this.db !== null;
|
|
10839
|
+
}
|
|
10840
|
+
catch {
|
|
10841
|
+
return false;
|
|
10842
|
+
}
|
|
10843
|
+
}
|
|
10844
|
+
getBackend() {
|
|
10845
|
+
return 'indexeddb';
|
|
10846
|
+
}
|
|
10847
|
+
destroy() {
|
|
10848
|
+
if (this.db) {
|
|
10849
|
+
this.db.close();
|
|
10850
|
+
this.db = null;
|
|
10851
|
+
}
|
|
10852
|
+
if (this.channel) {
|
|
10853
|
+
this.channel.close();
|
|
10854
|
+
this.channel = null;
|
|
10855
|
+
}
|
|
10856
|
+
}
|
|
10857
|
+
}
|
|
10858
|
+
/**
|
|
10859
|
+
* localStorage Storage Implementation (Fallback)
|
|
10860
|
+
*/
|
|
10861
|
+
class LocalStorageAdapter {
|
|
10862
|
+
constructor() {
|
|
10863
|
+
this.prefix = 'smartlinks_auth_';
|
|
10864
|
+
this.channel = null;
|
|
10865
|
+
// Set up BroadcastChannel for cross-tab communication
|
|
10866
|
+
if (typeof BroadcastChannel !== 'undefined') {
|
|
10867
|
+
this.channel = new BroadcastChannel(STORAGE_CHANNEL);
|
|
10868
|
+
}
|
|
10869
|
+
}
|
|
10870
|
+
broadcastChange(event) {
|
|
10871
|
+
if (this.channel) {
|
|
10872
|
+
try {
|
|
10873
|
+
this.channel.postMessage(event);
|
|
10874
|
+
}
|
|
10875
|
+
catch (error) {
|
|
10876
|
+
console.warn('Failed to broadcast storage change:', error);
|
|
10877
|
+
}
|
|
10878
|
+
}
|
|
10879
|
+
}
|
|
10880
|
+
async setItem(key, value) {
|
|
10881
|
+
try {
|
|
10882
|
+
const serialized = JSON.stringify(value);
|
|
10883
|
+
localStorage.setItem(this.prefix + key, serialized);
|
|
10884
|
+
this.broadcastChange({
|
|
10885
|
+
type: 'set',
|
|
10886
|
+
key,
|
|
10887
|
+
value,
|
|
10888
|
+
timestamp: Date.now(),
|
|
10889
|
+
});
|
|
10890
|
+
}
|
|
10891
|
+
catch (error) {
|
|
10892
|
+
console.error('localStorage setItem failed:', error);
|
|
10893
|
+
throw error;
|
|
10894
|
+
}
|
|
10895
|
+
}
|
|
10896
|
+
async getItem(key) {
|
|
10897
|
+
try {
|
|
10898
|
+
const stored = localStorage.getItem(this.prefix + key);
|
|
10899
|
+
if (!stored)
|
|
10900
|
+
return null;
|
|
10901
|
+
return JSON.parse(stored);
|
|
10902
|
+
}
|
|
10903
|
+
catch (error) {
|
|
10904
|
+
console.error('localStorage getItem failed:', error);
|
|
10905
|
+
return null;
|
|
10906
|
+
}
|
|
10907
|
+
}
|
|
10908
|
+
async removeItem(key) {
|
|
10909
|
+
try {
|
|
10910
|
+
localStorage.removeItem(this.prefix + key);
|
|
10911
|
+
this.broadcastChange({
|
|
10912
|
+
type: 'remove',
|
|
10913
|
+
key,
|
|
10914
|
+
timestamp: Date.now(),
|
|
10915
|
+
});
|
|
10916
|
+
}
|
|
10917
|
+
catch (error) {
|
|
10918
|
+
console.error('localStorage removeItem failed:', error);
|
|
10919
|
+
throw error;
|
|
10920
|
+
}
|
|
10921
|
+
}
|
|
10922
|
+
async clear() {
|
|
10923
|
+
try {
|
|
10924
|
+
const keys = Object.keys(localStorage);
|
|
10925
|
+
keys.forEach(key => {
|
|
10926
|
+
if (key.startsWith(this.prefix)) {
|
|
10927
|
+
localStorage.removeItem(key);
|
|
10928
|
+
}
|
|
10929
|
+
});
|
|
10930
|
+
this.broadcastChange({
|
|
10931
|
+
type: 'clear',
|
|
10932
|
+
timestamp: Date.now(),
|
|
10933
|
+
});
|
|
10934
|
+
}
|
|
10935
|
+
catch (error) {
|
|
10936
|
+
console.error('localStorage clear failed:', error);
|
|
10937
|
+
throw error;
|
|
10938
|
+
}
|
|
10939
|
+
}
|
|
10940
|
+
async isAvailable() {
|
|
10941
|
+
try {
|
|
10942
|
+
const testKey = '__smartlinks_test__';
|
|
10943
|
+
localStorage.setItem(testKey, 'test');
|
|
10944
|
+
localStorage.removeItem(testKey);
|
|
10945
|
+
return true;
|
|
10946
|
+
}
|
|
10947
|
+
catch {
|
|
10948
|
+
return false;
|
|
10949
|
+
}
|
|
10950
|
+
}
|
|
10951
|
+
getBackend() {
|
|
10952
|
+
return 'localstorage';
|
|
10953
|
+
}
|
|
10954
|
+
destroy() {
|
|
10955
|
+
if (this.channel) {
|
|
10956
|
+
this.channel.close();
|
|
10957
|
+
this.channel = null;
|
|
10958
|
+
}
|
|
10959
|
+
}
|
|
10960
|
+
}
|
|
10961
|
+
/**
|
|
10962
|
+
* In-Memory Storage Implementation (No Persistence)
|
|
10963
|
+
*/
|
|
10964
|
+
class InMemoryStorage {
|
|
10965
|
+
constructor() {
|
|
10966
|
+
this.storage = new Map();
|
|
10967
|
+
}
|
|
10968
|
+
async setItem(key, value) {
|
|
10969
|
+
this.storage.set(key, value);
|
|
10970
|
+
}
|
|
10971
|
+
async getItem(key) {
|
|
10972
|
+
return this.storage.has(key) ? this.storage.get(key) : null;
|
|
10973
|
+
}
|
|
10974
|
+
async removeItem(key) {
|
|
10975
|
+
this.storage.delete(key);
|
|
10976
|
+
}
|
|
10977
|
+
async clear() {
|
|
10978
|
+
this.storage.clear();
|
|
10979
|
+
}
|
|
10980
|
+
async isAvailable() {
|
|
10981
|
+
return true;
|
|
10982
|
+
}
|
|
10983
|
+
getBackend() {
|
|
10984
|
+
return 'none';
|
|
10985
|
+
}
|
|
10986
|
+
destroy() {
|
|
10987
|
+
this.storage.clear();
|
|
10988
|
+
}
|
|
10989
|
+
}
|
|
10990
|
+
/**
|
|
10991
|
+
* Storage Factory - Creates appropriate storage backend with automatic fallback
|
|
10992
|
+
*/
|
|
10993
|
+
class StorageFactory {
|
|
10994
|
+
static async createStorage() {
|
|
10995
|
+
if (this.instance) {
|
|
10996
|
+
return this.instance;
|
|
10997
|
+
}
|
|
10998
|
+
// Try IndexedDB first
|
|
10999
|
+
try {
|
|
11000
|
+
const indexedDB = new IndexedDBStorage();
|
|
11001
|
+
const available = await indexedDB.isAvailable();
|
|
11002
|
+
if (available) {
|
|
11003
|
+
console.log('[PersistentStorage] Using IndexedDB backend');
|
|
11004
|
+
this.instance = indexedDB;
|
|
11005
|
+
return indexedDB;
|
|
11006
|
+
}
|
|
11007
|
+
indexedDB.destroy();
|
|
11008
|
+
}
|
|
11009
|
+
catch (error) {
|
|
11010
|
+
console.warn('[PersistentStorage] IndexedDB failed:', error);
|
|
11011
|
+
}
|
|
11012
|
+
// Fallback to localStorage
|
|
11013
|
+
try {
|
|
11014
|
+
const localStorage = new LocalStorageAdapter();
|
|
11015
|
+
const available = await localStorage.isAvailable();
|
|
11016
|
+
if (available) {
|
|
11017
|
+
console.log('[PersistentStorage] Using localStorage backend (fallback)');
|
|
11018
|
+
this.instance = localStorage;
|
|
11019
|
+
return localStorage;
|
|
11020
|
+
}
|
|
11021
|
+
localStorage.destroy();
|
|
11022
|
+
}
|
|
11023
|
+
catch (error) {
|
|
11024
|
+
console.warn('[PersistentStorage] localStorage failed:', error);
|
|
11025
|
+
}
|
|
11026
|
+
// Final fallback to in-memory (no persistence)
|
|
11027
|
+
console.warn('[PersistentStorage] Using in-memory storage (no persistence)');
|
|
11028
|
+
this.instance = new InMemoryStorage();
|
|
11029
|
+
return this.instance;
|
|
11030
|
+
}
|
|
11031
|
+
static getInstance() {
|
|
11032
|
+
return this.instance;
|
|
11033
|
+
}
|
|
11034
|
+
static reset() {
|
|
11035
|
+
if (this.instance && 'destroy' in this.instance) {
|
|
11036
|
+
this.instance.destroy();
|
|
11037
|
+
}
|
|
11038
|
+
this.instance = null;
|
|
11039
|
+
}
|
|
11040
|
+
}
|
|
11041
|
+
StorageFactory.instance = null;
|
|
11042
|
+
/**
|
|
11043
|
+
* Main storage export - creates singleton storage instance
|
|
11044
|
+
*/
|
|
11045
|
+
let storageInstance = null;
|
|
11046
|
+
let storageInitPromise = null;
|
|
11047
|
+
async function getStorage() {
|
|
11048
|
+
if (storageInstance) {
|
|
11049
|
+
return storageInstance;
|
|
11050
|
+
}
|
|
11051
|
+
if (!storageInitPromise) {
|
|
11052
|
+
storageInitPromise = StorageFactory.createStorage();
|
|
11053
|
+
}
|
|
11054
|
+
storageInstance = await storageInitPromise;
|
|
11055
|
+
return storageInstance;
|
|
11056
|
+
}
|
|
11057
|
+
/**
|
|
11058
|
+
* Listen for storage changes from other tabs
|
|
11059
|
+
*/
|
|
11060
|
+
function onStorageChange(callback) {
|
|
11061
|
+
if (typeof BroadcastChannel === 'undefined') {
|
|
11062
|
+
console.warn('BroadcastChannel not supported, cross-tab sync disabled');
|
|
11063
|
+
return () => { };
|
|
11064
|
+
}
|
|
11065
|
+
const channel = new BroadcastChannel(STORAGE_CHANNEL);
|
|
11066
|
+
channel.onmessage = (event) => {
|
|
11067
|
+
callback(event.data);
|
|
11068
|
+
};
|
|
11069
|
+
return () => {
|
|
11070
|
+
channel.close();
|
|
11071
|
+
};
|
|
11072
|
+
}
|
|
11073
|
+
|
|
11074
|
+
const TOKEN_KEY = 'token';
|
|
11075
|
+
const USER_KEY = 'user';
|
|
11076
|
+
const ACCOUNT_DATA_KEY = 'account_data';
|
|
11077
|
+
const ACCOUNT_INFO_KEY = 'account_info';
|
|
11078
|
+
const ACCOUNT_INFO_TTL = 5 * 60 * 1000; // 5 minutes default
|
|
11079
|
+
/**
|
|
11080
|
+
* Token Storage Layer
|
|
11081
|
+
*
|
|
11082
|
+
* Manages authentication tokens, user data, and account data using
|
|
11083
|
+
* the persistent storage layer (IndexedDB + localStorage fallback).
|
|
11084
|
+
* All methods are async to support IndexedDB operations.
|
|
11085
|
+
*/
|
|
10678
11086
|
const tokenStorage = {
|
|
10679
|
-
saveToken(token, expiresAt) {
|
|
11087
|
+
async saveToken(token, expiresAt) {
|
|
11088
|
+
const storage = await getStorage();
|
|
10680
11089
|
const authToken = {
|
|
10681
11090
|
token,
|
|
10682
11091
|
expiresAt: expiresAt || Date.now() + 3600000, // Default 1 hour
|
|
10683
11092
|
};
|
|
10684
|
-
|
|
11093
|
+
await storage.setItem(TOKEN_KEY, authToken);
|
|
10685
11094
|
},
|
|
10686
|
-
getToken() {
|
|
10687
|
-
const
|
|
10688
|
-
|
|
11095
|
+
async getToken() {
|
|
11096
|
+
const storage = await getStorage();
|
|
11097
|
+
const authToken = await storage.getItem(TOKEN_KEY);
|
|
11098
|
+
if (!authToken)
|
|
10689
11099
|
return null;
|
|
10690
|
-
|
|
10691
|
-
|
|
10692
|
-
|
|
10693
|
-
if (authToken.expiresAt && authToken.expiresAt < Date.now()) {
|
|
10694
|
-
this.clearToken();
|
|
10695
|
-
return null;
|
|
10696
|
-
}
|
|
10697
|
-
return authToken;
|
|
10698
|
-
}
|
|
10699
|
-
catch {
|
|
11100
|
+
// Check if token is expired
|
|
11101
|
+
if (authToken.expiresAt && authToken.expiresAt < Date.now()) {
|
|
11102
|
+
await this.clearToken();
|
|
10700
11103
|
return null;
|
|
10701
11104
|
}
|
|
11105
|
+
return authToken;
|
|
10702
11106
|
},
|
|
10703
|
-
clearToken() {
|
|
10704
|
-
|
|
11107
|
+
async clearToken() {
|
|
11108
|
+
const storage = await getStorage();
|
|
11109
|
+
await storage.removeItem(TOKEN_KEY);
|
|
10705
11110
|
},
|
|
10706
|
-
saveUser(user) {
|
|
10707
|
-
|
|
11111
|
+
async saveUser(user) {
|
|
11112
|
+
const storage = await getStorage();
|
|
11113
|
+
await storage.setItem(USER_KEY, user);
|
|
10708
11114
|
},
|
|
10709
|
-
getUser() {
|
|
10710
|
-
const
|
|
10711
|
-
|
|
10712
|
-
return null;
|
|
10713
|
-
try {
|
|
10714
|
-
return JSON.parse(stored);
|
|
10715
|
-
}
|
|
10716
|
-
catch {
|
|
10717
|
-
return null;
|
|
10718
|
-
}
|
|
11115
|
+
async getUser() {
|
|
11116
|
+
const storage = await getStorage();
|
|
11117
|
+
return await storage.getItem(USER_KEY);
|
|
10719
11118
|
},
|
|
10720
|
-
clearUser() {
|
|
10721
|
-
|
|
11119
|
+
async clearUser() {
|
|
11120
|
+
const storage = await getStorage();
|
|
11121
|
+
await storage.removeItem(USER_KEY);
|
|
10722
11122
|
},
|
|
10723
|
-
clearAll() {
|
|
10724
|
-
this.clearToken();
|
|
10725
|
-
this.clearUser();
|
|
10726
|
-
this.clearAccountData();
|
|
11123
|
+
async clearAll() {
|
|
11124
|
+
await this.clearToken();
|
|
11125
|
+
await this.clearUser();
|
|
11126
|
+
await this.clearAccountData();
|
|
11127
|
+
await this.clearAccountInfo();
|
|
10727
11128
|
},
|
|
10728
|
-
saveAccountData(data) {
|
|
10729
|
-
|
|
11129
|
+
async saveAccountData(data) {
|
|
11130
|
+
const storage = await getStorage();
|
|
11131
|
+
await storage.setItem(ACCOUNT_DATA_KEY, data);
|
|
10730
11132
|
},
|
|
10731
|
-
getAccountData() {
|
|
10732
|
-
const
|
|
10733
|
-
|
|
10734
|
-
|
|
10735
|
-
|
|
10736
|
-
|
|
10737
|
-
|
|
10738
|
-
|
|
11133
|
+
async getAccountData() {
|
|
11134
|
+
const storage = await getStorage();
|
|
11135
|
+
return await storage.getItem(ACCOUNT_DATA_KEY);
|
|
11136
|
+
},
|
|
11137
|
+
async clearAccountData() {
|
|
11138
|
+
const storage = await getStorage();
|
|
11139
|
+
await storage.removeItem(ACCOUNT_DATA_KEY);
|
|
11140
|
+
},
|
|
11141
|
+
async saveAccountInfo(accountInfo, ttl = ACCOUNT_INFO_TTL) {
|
|
11142
|
+
const storage = await getStorage();
|
|
11143
|
+
const cachedData = {
|
|
11144
|
+
data: accountInfo,
|
|
11145
|
+
cachedAt: Date.now(),
|
|
11146
|
+
expiresAt: Date.now() + ttl,
|
|
11147
|
+
};
|
|
11148
|
+
await storage.setItem(ACCOUNT_INFO_KEY, cachedData);
|
|
11149
|
+
},
|
|
11150
|
+
async getAccountInfo() {
|
|
11151
|
+
const storage = await getStorage();
|
|
11152
|
+
const cached = await storage.getItem(ACCOUNT_INFO_KEY);
|
|
11153
|
+
if (!cached)
|
|
10739
11154
|
return null;
|
|
10740
|
-
|
|
11155
|
+
const isStale = cached.expiresAt < Date.now();
|
|
11156
|
+
return {
|
|
11157
|
+
data: cached.data,
|
|
11158
|
+
isStale,
|
|
11159
|
+
};
|
|
10741
11160
|
},
|
|
10742
|
-
|
|
10743
|
-
|
|
11161
|
+
async clearAccountInfo() {
|
|
11162
|
+
const storage = await getStorage();
|
|
11163
|
+
await storage.removeItem(ACCOUNT_INFO_KEY);
|
|
10744
11164
|
},
|
|
10745
11165
|
};
|
|
10746
11166
|
|
|
10747
11167
|
const AuthContext = createContext(undefined);
|
|
10748
|
-
const AuthProvider = ({ children }) => {
|
|
11168
|
+
const AuthProvider = ({ children, accountCacheTTL = 5 * 60 * 1000, preloadAccountInfo = false }) => {
|
|
10749
11169
|
const [user, setUser] = useState(null);
|
|
10750
11170
|
const [token, setToken] = useState(null);
|
|
10751
11171
|
const [accountData, setAccountData] = useState(null);
|
|
11172
|
+
const [accountInfo, setAccountInfo] = useState(null);
|
|
10752
11173
|
const [isLoading, setIsLoading] = useState(true);
|
|
10753
|
-
|
|
11174
|
+
const callbacksRef = useRef(new Set());
|
|
11175
|
+
// Notify all subscribers of auth state changes
|
|
11176
|
+
const notifyAuthStateChange = useCallback((type, currentUser, currentToken, currentAccountData, currentAccountInfo) => {
|
|
11177
|
+
callbacksRef.current.forEach(callback => {
|
|
11178
|
+
try {
|
|
11179
|
+
callback({
|
|
11180
|
+
type,
|
|
11181
|
+
user: currentUser,
|
|
11182
|
+
token: currentToken,
|
|
11183
|
+
accountData: currentAccountData,
|
|
11184
|
+
accountInfo: currentAccountInfo
|
|
11185
|
+
});
|
|
11186
|
+
}
|
|
11187
|
+
catch (error) {
|
|
11188
|
+
console.error('[AuthContext] Error in auth state change callback:', error);
|
|
11189
|
+
}
|
|
11190
|
+
});
|
|
11191
|
+
}, []);
|
|
11192
|
+
// Initialize auth state from persistent storage
|
|
11193
|
+
useEffect(() => {
|
|
11194
|
+
const initializeAuth = async () => {
|
|
11195
|
+
try {
|
|
11196
|
+
const storedToken = await tokenStorage.getToken();
|
|
11197
|
+
const storedUser = await tokenStorage.getUser();
|
|
11198
|
+
const storedAccountData = await tokenStorage.getAccountData();
|
|
11199
|
+
if (storedToken && storedUser) {
|
|
11200
|
+
setToken(storedToken.token);
|
|
11201
|
+
setUser(storedUser);
|
|
11202
|
+
setAccountData(storedAccountData);
|
|
11203
|
+
// Set bearer token in global Smartlinks SDK via auth.verifyToken
|
|
11204
|
+
smartlinks.auth.verifyToken(storedToken.token).catch(err => {
|
|
11205
|
+
console.warn('Failed to restore bearer token on init:', err);
|
|
11206
|
+
});
|
|
11207
|
+
}
|
|
11208
|
+
// Load cached account info if available
|
|
11209
|
+
const cachedAccountInfo = await tokenStorage.getAccountInfo();
|
|
11210
|
+
if (cachedAccountInfo && !cachedAccountInfo.isStale) {
|
|
11211
|
+
setAccountInfo(cachedAccountInfo.data);
|
|
11212
|
+
}
|
|
11213
|
+
}
|
|
11214
|
+
catch (error) {
|
|
11215
|
+
console.error('Failed to initialize auth from storage:', error);
|
|
11216
|
+
}
|
|
11217
|
+
finally {
|
|
11218
|
+
setIsLoading(false);
|
|
11219
|
+
}
|
|
11220
|
+
};
|
|
11221
|
+
initializeAuth();
|
|
11222
|
+
}, []);
|
|
11223
|
+
// Cross-tab synchronization - listen for auth changes in other tabs
|
|
10754
11224
|
useEffect(() => {
|
|
10755
|
-
|
|
10756
|
-
const
|
|
10757
|
-
|
|
10758
|
-
|
|
10759
|
-
|
|
10760
|
-
|
|
10761
|
-
|
|
11225
|
+
console.log('[AuthContext] Setting up cross-tab synchronization');
|
|
11226
|
+
const unsubscribe = onStorageChange(async (event) => {
|
|
11227
|
+
console.log('[AuthContext] Cross-tab storage event:', event.type, event.key);
|
|
11228
|
+
try {
|
|
11229
|
+
if (event.type === 'clear') {
|
|
11230
|
+
// Another tab cleared all storage (logout)
|
|
11231
|
+
console.log('[AuthContext] Detected logout in another tab');
|
|
11232
|
+
setToken(null);
|
|
11233
|
+
setUser(null);
|
|
11234
|
+
setAccountData(null);
|
|
11235
|
+
setAccountInfo(null);
|
|
11236
|
+
smartlinks.auth.logout();
|
|
11237
|
+
notifyAuthStateChange('CROSS_TAB_SYNC', null, null, null);
|
|
11238
|
+
}
|
|
11239
|
+
else if (event.type === 'remove' && (event.key === 'token' || event.key === 'user')) {
|
|
11240
|
+
// Another tab removed token or user (logout)
|
|
11241
|
+
console.log('[AuthContext] Detected token/user removal in another tab');
|
|
11242
|
+
setToken(null);
|
|
11243
|
+
setUser(null);
|
|
11244
|
+
setAccountData(null);
|
|
11245
|
+
setAccountInfo(null);
|
|
11246
|
+
smartlinks.auth.logout();
|
|
11247
|
+
notifyAuthStateChange('CROSS_TAB_SYNC', null, null, null);
|
|
11248
|
+
}
|
|
11249
|
+
else if (event.type === 'set' && event.key === 'token') {
|
|
11250
|
+
// Another tab set a new token (login)
|
|
11251
|
+
console.log('[AuthContext] Detected login in another tab');
|
|
11252
|
+
const storedToken = await tokenStorage.getToken();
|
|
11253
|
+
const storedUser = await tokenStorage.getUser();
|
|
11254
|
+
const storedAccountData = await tokenStorage.getAccountData();
|
|
11255
|
+
if (storedToken && storedUser) {
|
|
11256
|
+
setToken(storedToken.token);
|
|
11257
|
+
setUser(storedUser);
|
|
11258
|
+
setAccountData(storedAccountData);
|
|
11259
|
+
// Set bearer token in global Smartlinks SDK
|
|
11260
|
+
smartlinks.auth.verifyToken(storedToken.token).catch(err => {
|
|
11261
|
+
console.warn('[AuthContext] Failed to restore bearer token from cross-tab sync:', err);
|
|
11262
|
+
});
|
|
11263
|
+
notifyAuthStateChange('CROSS_TAB_SYNC', storedUser, storedToken.token, storedAccountData);
|
|
11264
|
+
}
|
|
11265
|
+
}
|
|
11266
|
+
else if (event.type === 'set' && event.key === 'account_info') {
|
|
11267
|
+
// Another tab fetched fresh account info
|
|
11268
|
+
const cached = await tokenStorage.getAccountInfo();
|
|
11269
|
+
if (cached && !cached.isStale) {
|
|
11270
|
+
setAccountInfo(cached.data);
|
|
11271
|
+
console.log('[AuthContext] Account info synced from another tab');
|
|
11272
|
+
}
|
|
11273
|
+
}
|
|
11274
|
+
}
|
|
11275
|
+
catch (error) {
|
|
11276
|
+
console.error('[AuthContext] Error handling cross-tab sync:', error);
|
|
11277
|
+
}
|
|
11278
|
+
});
|
|
11279
|
+
return () => {
|
|
11280
|
+
console.log('[AuthContext] Cleaning up cross-tab synchronization');
|
|
11281
|
+
unsubscribe();
|
|
11282
|
+
};
|
|
11283
|
+
}, [notifyAuthStateChange]);
|
|
11284
|
+
const login = useCallback(async (authToken, authUser, authAccountData) => {
|
|
11285
|
+
try {
|
|
11286
|
+
// Store token, user, and account data
|
|
11287
|
+
await tokenStorage.saveToken(authToken);
|
|
11288
|
+
await tokenStorage.saveUser(authUser);
|
|
11289
|
+
if (authAccountData) {
|
|
11290
|
+
await tokenStorage.saveAccountData(authAccountData);
|
|
11291
|
+
}
|
|
11292
|
+
setToken(authToken);
|
|
11293
|
+
setUser(authUser);
|
|
11294
|
+
setAccountData(authAccountData || null);
|
|
10762
11295
|
// Set bearer token in global Smartlinks SDK via auth.verifyToken
|
|
10763
|
-
|
|
10764
|
-
|
|
11296
|
+
// This both validates the token and sets it for future API calls
|
|
11297
|
+
smartlinks.auth.verifyToken(authToken).catch(err => {
|
|
11298
|
+
console.warn('Failed to set bearer token on login:', err);
|
|
10765
11299
|
});
|
|
11300
|
+
notifyAuthStateChange('LOGIN', authUser, authToken, authAccountData || null);
|
|
11301
|
+
// Optionally preload account info on login
|
|
11302
|
+
if (preloadAccountInfo) {
|
|
11303
|
+
// Preload after login completes (non-blocking)
|
|
11304
|
+
getAccount(true).catch(error => {
|
|
11305
|
+
console.warn('[AuthContext] Failed to preload account info:', error);
|
|
11306
|
+
});
|
|
11307
|
+
}
|
|
10766
11308
|
}
|
|
10767
|
-
|
|
10768
|
-
|
|
10769
|
-
|
|
10770
|
-
|
|
10771
|
-
|
|
10772
|
-
tokenStorage.saveUser(authUser);
|
|
10773
|
-
if (authAccountData) {
|
|
10774
|
-
tokenStorage.saveAccountData(authAccountData);
|
|
10775
|
-
}
|
|
10776
|
-
setToken(authToken);
|
|
10777
|
-
setUser(authUser);
|
|
10778
|
-
setAccountData(authAccountData || null);
|
|
10779
|
-
// Set bearer token in global Smartlinks SDK via auth.verifyToken
|
|
10780
|
-
// This both validates the token and sets it for future API calls
|
|
10781
|
-
smartlinks.auth.verifyToken(authToken).catch(err => {
|
|
10782
|
-
console.warn('Failed to set bearer token on login:', err);
|
|
10783
|
-
});
|
|
10784
|
-
}, []);
|
|
11309
|
+
catch (error) {
|
|
11310
|
+
console.error('Failed to save auth data to storage:', error);
|
|
11311
|
+
throw error;
|
|
11312
|
+
}
|
|
11313
|
+
}, [notifyAuthStateChange, preloadAccountInfo]);
|
|
10785
11314
|
const logout = useCallback(async () => {
|
|
10786
|
-
|
|
10787
|
-
|
|
10788
|
-
|
|
10789
|
-
|
|
10790
|
-
|
|
10791
|
-
|
|
10792
|
-
|
|
10793
|
-
|
|
10794
|
-
|
|
10795
|
-
|
|
11315
|
+
try {
|
|
11316
|
+
// Clear persistent storage
|
|
11317
|
+
await tokenStorage.clearAll();
|
|
11318
|
+
setToken(null);
|
|
11319
|
+
setUser(null);
|
|
11320
|
+
setAccountData(null);
|
|
11321
|
+
setAccountInfo(null);
|
|
11322
|
+
// Clear bearer token from global Smartlinks SDK
|
|
11323
|
+
smartlinks.auth.logout();
|
|
11324
|
+
notifyAuthStateChange('LOGOUT', null, null, null);
|
|
11325
|
+
}
|
|
11326
|
+
catch (error) {
|
|
11327
|
+
console.error('Failed to clear auth data from storage:', error);
|
|
11328
|
+
}
|
|
11329
|
+
}, [notifyAuthStateChange]);
|
|
11330
|
+
const getToken = useCallback(async () => {
|
|
11331
|
+
const storedToken = await tokenStorage.getToken();
|
|
10796
11332
|
return storedToken ? storedToken.token : null;
|
|
10797
11333
|
}, []);
|
|
10798
11334
|
const refreshToken = useCallback(async () => {
|
|
10799
11335
|
throw new Error('Token refresh must be implemented via your backend API');
|
|
10800
11336
|
}, []);
|
|
11337
|
+
// Get account with intelligent caching
|
|
11338
|
+
const getAccount = useCallback(async (forceRefresh = false) => {
|
|
11339
|
+
try {
|
|
11340
|
+
// Check if user is authenticated
|
|
11341
|
+
if (!token) {
|
|
11342
|
+
throw new Error('Not authenticated. Please login first.');
|
|
11343
|
+
}
|
|
11344
|
+
// Check cache unless force refresh
|
|
11345
|
+
if (!forceRefresh) {
|
|
11346
|
+
const cached = await tokenStorage.getAccountInfo();
|
|
11347
|
+
if (cached && !cached.isStale) {
|
|
11348
|
+
console.log('[AuthContext] Returning cached account info');
|
|
11349
|
+
return cached.data;
|
|
11350
|
+
}
|
|
11351
|
+
}
|
|
11352
|
+
// Fetch fresh data from API
|
|
11353
|
+
console.log('[AuthContext] Fetching fresh account info from API');
|
|
11354
|
+
const freshAccountInfo = await smartlinks.auth.getAccount();
|
|
11355
|
+
// Cache the fresh data
|
|
11356
|
+
await tokenStorage.saveAccountInfo(freshAccountInfo, accountCacheTTL);
|
|
11357
|
+
setAccountInfo(freshAccountInfo);
|
|
11358
|
+
notifyAuthStateChange('ACCOUNT_REFRESH', user, token, accountData, freshAccountInfo);
|
|
11359
|
+
return freshAccountInfo;
|
|
11360
|
+
}
|
|
11361
|
+
catch (error) {
|
|
11362
|
+
console.error('[AuthContext] Failed to get account info:', error);
|
|
11363
|
+
// Fallback to stale cache if API fails
|
|
11364
|
+
const cached = await tokenStorage.getAccountInfo();
|
|
11365
|
+
if (cached) {
|
|
11366
|
+
console.warn('[AuthContext] Returning stale cached data due to API error');
|
|
11367
|
+
return cached.data;
|
|
11368
|
+
}
|
|
11369
|
+
throw error;
|
|
11370
|
+
}
|
|
11371
|
+
}, [token, accountCacheTTL, user, accountData, notifyAuthStateChange]);
|
|
11372
|
+
// Convenience method for explicit refresh
|
|
11373
|
+
const refreshAccount = useCallback(async () => {
|
|
11374
|
+
return await getAccount(true);
|
|
11375
|
+
}, [getAccount]);
|
|
11376
|
+
// Clear account cache
|
|
11377
|
+
const clearAccountCache = useCallback(async () => {
|
|
11378
|
+
await tokenStorage.clearAccountInfo();
|
|
11379
|
+
setAccountInfo(null);
|
|
11380
|
+
}, []);
|
|
11381
|
+
const onAuthStateChange = useCallback((callback) => {
|
|
11382
|
+
callbacksRef.current.add(callback);
|
|
11383
|
+
// Return unsubscribe function
|
|
11384
|
+
return () => {
|
|
11385
|
+
callbacksRef.current.delete(callback);
|
|
11386
|
+
};
|
|
11387
|
+
}, []);
|
|
10801
11388
|
const value = {
|
|
10802
11389
|
user,
|
|
10803
11390
|
token,
|
|
10804
11391
|
accountData,
|
|
11392
|
+
accountInfo,
|
|
10805
11393
|
isAuthenticated: !!token && !!user,
|
|
10806
11394
|
isLoading,
|
|
10807
11395
|
login,
|
|
10808
11396
|
logout,
|
|
10809
11397
|
getToken,
|
|
10810
11398
|
refreshToken,
|
|
11399
|
+
getAccount,
|
|
11400
|
+
refreshAccount,
|
|
11401
|
+
clearAccountCache,
|
|
11402
|
+
onAuthStateChange,
|
|
10811
11403
|
};
|
|
10812
11404
|
return jsx(AuthContext.Provider, { value: value, children: children });
|
|
10813
11405
|
};
|
|
@@ -10819,997 +11411,1093 @@ const useAuth = () => {
|
|
|
10819
11411
|
return context;
|
|
10820
11412
|
};
|
|
10821
11413
|
|
|
10822
|
-
|
|
10823
|
-
|
|
11414
|
+
// Default Smartlinks Google OAuth Client ID (public - safe to expose)
|
|
11415
|
+
const DEFAULT_GOOGLE_CLIENT_ID = '696509063554-jdlbjl8vsjt7cr0vgkjkjf3ffnvi3a70.apps.googleusercontent.com';
|
|
11416
|
+
const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAuthSuccess, onAuthError, enabledProviders = ['email', 'google', 'phone'], initialMode = 'login', redirectUrl, theme = 'light', className, customization, skipConfigFetch = false, minimal = false, }) => {
|
|
11417
|
+
const [mode, setMode] = useState(initialMode);
|
|
10824
11418
|
const [loading, setLoading] = useState(false);
|
|
10825
|
-
const [profile, setProfile] = useState(null);
|
|
10826
11419
|
const [error, setError] = useState();
|
|
10827
|
-
const [
|
|
10828
|
-
|
|
10829
|
-
const [
|
|
10830
|
-
|
|
10831
|
-
const [
|
|
10832
|
-
|
|
10833
|
-
const [
|
|
10834
|
-
const [
|
|
10835
|
-
|
|
10836
|
-
const [
|
|
10837
|
-
const [
|
|
10838
|
-
const
|
|
10839
|
-
|
|
10840
|
-
|
|
10841
|
-
|
|
10842
|
-
const [phoneCodeSent, setPhoneCodeSent] = useState(false);
|
|
10843
|
-
// Account deletion state
|
|
10844
|
-
const [deletePassword, setDeletePassword] = useState('');
|
|
10845
|
-
const [deleteConfirmText, setDeleteConfirmText] = useState('');
|
|
10846
|
-
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
|
10847
|
-
const { showProfileSection = true, showEmailSection = true, showPasswordSection = true, showPhoneSection = true, showDeleteAccount = false, } = customization;
|
|
10848
|
-
// Reinitialize Smartlinks SDK when apiEndpoint changes
|
|
11420
|
+
const [resetSuccess, setResetSuccess] = useState(false);
|
|
11421
|
+
const [authSuccess, setAuthSuccess] = useState(false);
|
|
11422
|
+
const [successMessage, setSuccessMessage] = useState();
|
|
11423
|
+
const [showResendVerification, setShowResendVerification] = useState(false);
|
|
11424
|
+
const [resendEmail, setResendEmail] = useState();
|
|
11425
|
+
const [showRequestNewReset, setShowRequestNewReset] = useState(false);
|
|
11426
|
+
const [resetRequestEmail, setResetRequestEmail] = useState();
|
|
11427
|
+
const [resetToken, setResetToken] = useState(); // Store the reset token from URL
|
|
11428
|
+
const [config, setConfig] = useState(null);
|
|
11429
|
+
const [configLoading, setConfigLoading] = useState(!skipConfigFetch);
|
|
11430
|
+
const [showEmailForm, setShowEmailForm] = useState(false); // Track if email form should be shown when emailDisplayMode is 'button'
|
|
11431
|
+
const api = new AuthAPI(apiEndpoint, clientId, clientName);
|
|
11432
|
+
const auth = useAuth();
|
|
11433
|
+
// Reinitialize Smartlinks SDK when apiEndpoint changes (for test/dev scenarios)
|
|
11434
|
+
// IMPORTANT: Preserve bearer token during reinitialization
|
|
10849
11435
|
useEffect(() => {
|
|
10850
|
-
|
|
10851
|
-
|
|
10852
|
-
|
|
10853
|
-
|
|
10854
|
-
|
|
10855
|
-
|
|
10856
|
-
|
|
10857
|
-
|
|
10858
|
-
|
|
11436
|
+
const reinitializeWithToken = async () => {
|
|
11437
|
+
if (apiEndpoint) {
|
|
11438
|
+
// Get current token before reinitializing
|
|
11439
|
+
const currentToken = await auth.getToken();
|
|
11440
|
+
smartlinks.initializeApi({
|
|
11441
|
+
baseURL: apiEndpoint,
|
|
11442
|
+
proxyMode: false, // Direct API calls when custom endpoint is provided
|
|
11443
|
+
ngrokSkipBrowserWarning: true,
|
|
11444
|
+
});
|
|
11445
|
+
// Restore bearer token after reinitialization using auth.verifyToken
|
|
11446
|
+
if (currentToken) {
|
|
11447
|
+
smartlinks.auth.verifyToken(currentToken).catch(err => {
|
|
11448
|
+
console.warn('Failed to restore bearer token after reinit:', err);
|
|
11449
|
+
});
|
|
11450
|
+
}
|
|
11451
|
+
}
|
|
11452
|
+
};
|
|
11453
|
+
reinitializeWithToken();
|
|
11454
|
+
}, [apiEndpoint, auth]);
|
|
11455
|
+
// Get the effective redirect URL (use prop or default to current page)
|
|
11456
|
+
const getRedirectUrl = () => {
|
|
11457
|
+
if (redirectUrl)
|
|
11458
|
+
return redirectUrl;
|
|
11459
|
+
// Get the full current URL including hash routes
|
|
11460
|
+
// Remove any existing query parameters to avoid duplication
|
|
11461
|
+
const currentUrl = window.location.href.split('?')[0];
|
|
11462
|
+
return currentUrl;
|
|
11463
|
+
};
|
|
11464
|
+
// Fetch UI configuration
|
|
10859
11465
|
useEffect(() => {
|
|
10860
|
-
|
|
10861
|
-
|
|
10862
|
-
const loadProfile = async () => {
|
|
10863
|
-
if (!auth.isAuthenticated) {
|
|
10864
|
-
setError('You must be logged in to manage your account');
|
|
11466
|
+
if (skipConfigFetch) {
|
|
11467
|
+
setConfig(customization || {});
|
|
10865
11468
|
return;
|
|
10866
11469
|
}
|
|
11470
|
+
const fetchConfig = async () => {
|
|
11471
|
+
try {
|
|
11472
|
+
// Check localStorage cache first
|
|
11473
|
+
const cacheKey = `auth_ui_config_${clientId || 'default'}`;
|
|
11474
|
+
const cached = localStorage.getItem(cacheKey);
|
|
11475
|
+
if (cached) {
|
|
11476
|
+
const { config: cachedConfig, timestamp } = JSON.parse(cached);
|
|
11477
|
+
const age = Date.now() - timestamp;
|
|
11478
|
+
// Use cache if less than 1 hour old
|
|
11479
|
+
if (age < 3600000) {
|
|
11480
|
+
setConfig({ ...cachedConfig, ...customization });
|
|
11481
|
+
setConfigLoading(false);
|
|
11482
|
+
// Fetch in background to update cache
|
|
11483
|
+
api.fetchConfig().then(freshConfig => {
|
|
11484
|
+
localStorage.setItem(cacheKey, JSON.stringify({
|
|
11485
|
+
config: freshConfig,
|
|
11486
|
+
timestamp: Date.now()
|
|
11487
|
+
}));
|
|
11488
|
+
});
|
|
11489
|
+
return;
|
|
11490
|
+
}
|
|
11491
|
+
}
|
|
11492
|
+
// Fetch from API
|
|
11493
|
+
const fetchedConfig = await api.fetchConfig();
|
|
11494
|
+
// Merge with customization props (props take precedence)
|
|
11495
|
+
const mergedConfig = { ...fetchedConfig, ...customization };
|
|
11496
|
+
setConfig(mergedConfig);
|
|
11497
|
+
// Cache the fetched config
|
|
11498
|
+
localStorage.setItem(cacheKey, JSON.stringify({
|
|
11499
|
+
config: fetchedConfig,
|
|
11500
|
+
timestamp: Date.now()
|
|
11501
|
+
}));
|
|
11502
|
+
}
|
|
11503
|
+
catch (err) {
|
|
11504
|
+
console.error('Failed to fetch config:', err);
|
|
11505
|
+
setConfig(customization || {});
|
|
11506
|
+
}
|
|
11507
|
+
finally {
|
|
11508
|
+
setConfigLoading(false);
|
|
11509
|
+
}
|
|
11510
|
+
};
|
|
11511
|
+
fetchConfig();
|
|
11512
|
+
}, [apiEndpoint, clientId, customization, skipConfigFetch]);
|
|
11513
|
+
// Reset showEmailForm when mode changes away from login/register
|
|
11514
|
+
useEffect(() => {
|
|
11515
|
+
if (mode !== 'login' && mode !== 'register') {
|
|
11516
|
+
setShowEmailForm(false);
|
|
11517
|
+
}
|
|
11518
|
+
}, [mode]);
|
|
11519
|
+
// Handle URL-based auth flows (email verification, password reset)
|
|
11520
|
+
useEffect(() => {
|
|
11521
|
+
// Helper to get URL parameters from either hash or search
|
|
11522
|
+
const getUrlParams = () => {
|
|
11523
|
+
// First check if there are params in the hash (for hash routing)
|
|
11524
|
+
const hash = window.location.hash;
|
|
11525
|
+
const hashQueryIndex = hash.indexOf('?');
|
|
11526
|
+
if (hashQueryIndex !== -1) {
|
|
11527
|
+
// Extract query string from hash (e.g., #/test?mode=verifyEmail&token=abc)
|
|
11528
|
+
const hashQuery = hash.substring(hashQueryIndex + 1);
|
|
11529
|
+
return new URLSearchParams(hashQuery);
|
|
11530
|
+
}
|
|
11531
|
+
// Fall back to regular search params (for non-hash routing)
|
|
11532
|
+
return new URLSearchParams(window.location.search);
|
|
11533
|
+
};
|
|
11534
|
+
const params = getUrlParams();
|
|
11535
|
+
const urlMode = params.get('mode');
|
|
11536
|
+
const token = params.get('token');
|
|
11537
|
+
console.log('URL params detected:', { urlMode, token, hash: window.location.hash, search: window.location.search });
|
|
11538
|
+
if (urlMode && token) {
|
|
11539
|
+
handleURLBasedAuth(urlMode, token);
|
|
11540
|
+
}
|
|
11541
|
+
}, []);
|
|
11542
|
+
const handleURLBasedAuth = async (urlMode, token) => {
|
|
10867
11543
|
setLoading(true);
|
|
10868
11544
|
setError(undefined);
|
|
10869
11545
|
try {
|
|
10870
|
-
|
|
10871
|
-
|
|
10872
|
-
|
|
10873
|
-
|
|
10874
|
-
|
|
10875
|
-
|
|
10876
|
-
|
|
10877
|
-
|
|
10878
|
-
|
|
10879
|
-
|
|
10880
|
-
|
|
10881
|
-
|
|
10882
|
-
|
|
10883
|
-
|
|
10884
|
-
|
|
11546
|
+
if (urlMode === 'verifyEmail') {
|
|
11547
|
+
console.log('Verifying email with token:', token);
|
|
11548
|
+
const response = await api.verifyEmailWithToken(token);
|
|
11549
|
+
// Get email verification mode from response or config
|
|
11550
|
+
const verificationMode = response.emailVerificationMode || config?.emailVerification?.mode || 'verify-then-auto-login';
|
|
11551
|
+
if ((verificationMode === 'verify-then-auto-login' || verificationMode === 'immediate') && response.token) {
|
|
11552
|
+
// Auto-login modes: Log the user in immediately if token is provided
|
|
11553
|
+
auth.login(response.token, response.user, response.accountData);
|
|
11554
|
+
setAuthSuccess(true);
|
|
11555
|
+
setSuccessMessage('Email verified successfully! You are now logged in.');
|
|
11556
|
+
onAuthSuccess(response.token, response.user, response.accountData);
|
|
11557
|
+
// Clear the URL parameters
|
|
11558
|
+
const cleanUrl = window.location.href.split('?')[0];
|
|
11559
|
+
window.history.replaceState({}, document.title, cleanUrl);
|
|
11560
|
+
// Redirect after a brief delay to show success message
|
|
11561
|
+
if (redirectUrl) {
|
|
11562
|
+
setTimeout(() => {
|
|
11563
|
+
window.location.href = redirectUrl;
|
|
11564
|
+
}, 2000);
|
|
11565
|
+
}
|
|
11566
|
+
}
|
|
11567
|
+
else {
|
|
11568
|
+
// verify-then-manual-login mode or no token: Show success but require manual login
|
|
11569
|
+
setAuthSuccess(true);
|
|
11570
|
+
setSuccessMessage('Email verified successfully! Please log in with your credentials.');
|
|
11571
|
+
// Clear the URL parameters
|
|
11572
|
+
const cleanUrl = window.location.href.split('?')[0];
|
|
11573
|
+
window.history.replaceState({}, document.title, cleanUrl);
|
|
11574
|
+
// Switch back to login mode after a delay
|
|
11575
|
+
setTimeout(() => {
|
|
11576
|
+
setAuthSuccess(false);
|
|
11577
|
+
setMode('login');
|
|
11578
|
+
}, 3000);
|
|
11579
|
+
}
|
|
11580
|
+
}
|
|
11581
|
+
else if (urlMode === 'resetPassword') {
|
|
11582
|
+
console.log('Verifying reset token:', token);
|
|
11583
|
+
// Verify token is valid, then show password reset form
|
|
11584
|
+
await api.verifyResetToken(token);
|
|
11585
|
+
setResetToken(token); // Store token for use in password reset
|
|
11586
|
+
setMode('reset-password');
|
|
11587
|
+
// Clear the URL parameters
|
|
11588
|
+
const cleanUrl = window.location.href.split('?')[0];
|
|
11589
|
+
window.history.replaceState({}, document.title, cleanUrl);
|
|
11590
|
+
}
|
|
11591
|
+
else if (urlMode === 'magicLink') {
|
|
11592
|
+
console.log('Verifying magic link token:', token);
|
|
11593
|
+
const response = await api.verifyMagicLink(token);
|
|
11594
|
+
// Auto-login with magic link if token is provided
|
|
11595
|
+
if (response.token) {
|
|
11596
|
+
auth.login(response.token, response.user, response.accountData);
|
|
11597
|
+
setAuthSuccess(true);
|
|
11598
|
+
setSuccessMessage('Magic link verified! You are now logged in.');
|
|
11599
|
+
onAuthSuccess(response.token, response.user, response.accountData);
|
|
11600
|
+
// Clear the URL parameters
|
|
11601
|
+
const cleanUrl = window.location.href.split('?')[0];
|
|
11602
|
+
window.history.replaceState({}, document.title, cleanUrl);
|
|
11603
|
+
// Redirect after a brief delay to show success message
|
|
11604
|
+
if (redirectUrl) {
|
|
11605
|
+
setTimeout(() => {
|
|
11606
|
+
window.location.href = redirectUrl;
|
|
11607
|
+
}, 2000);
|
|
11608
|
+
}
|
|
11609
|
+
}
|
|
11610
|
+
else {
|
|
11611
|
+
throw new Error('Authentication failed - no token received');
|
|
11612
|
+
}
|
|
11613
|
+
}
|
|
10885
11614
|
}
|
|
10886
11615
|
catch (err) {
|
|
10887
|
-
|
|
10888
|
-
|
|
10889
|
-
|
|
11616
|
+
console.error('URL-based auth error:', err);
|
|
11617
|
+
const errorMessage = err instanceof Error ? err.message : 'An error occurred';
|
|
11618
|
+
// If it's an email verification error (expired/invalid token), show resend option
|
|
11619
|
+
if (urlMode === 'verifyEmail') {
|
|
11620
|
+
setError(`${errorMessage} - Please enter your email below to receive a new verification link.`);
|
|
11621
|
+
setShowResendVerification(true);
|
|
11622
|
+
setMode('login'); // Show the login form UI
|
|
11623
|
+
// Clear the URL parameters
|
|
11624
|
+
const cleanUrl = window.location.href.split('?')[0];
|
|
11625
|
+
window.history.replaceState({}, document.title, cleanUrl);
|
|
11626
|
+
}
|
|
11627
|
+
else if (urlMode === 'resetPassword') {
|
|
11628
|
+
// If password reset token is invalid/expired, show request new reset link option
|
|
11629
|
+
setError(`${errorMessage} - Please enter your email below to receive a new password reset link.`);
|
|
11630
|
+
setShowRequestNewReset(true);
|
|
11631
|
+
setMode('login');
|
|
11632
|
+
// Clear the URL parameters
|
|
11633
|
+
const cleanUrl = window.location.href.split('?')[0];
|
|
11634
|
+
window.history.replaceState({}, document.title, cleanUrl);
|
|
11635
|
+
}
|
|
11636
|
+
else if (urlMode === 'magicLink') {
|
|
11637
|
+
// If magic link is invalid/expired
|
|
11638
|
+
setError(`${errorMessage} - Please request a new magic link below.`);
|
|
11639
|
+
setMode('magic-link');
|
|
11640
|
+
// Clear the URL parameters
|
|
11641
|
+
const cleanUrl = window.location.href.split('?')[0];
|
|
11642
|
+
window.history.replaceState({}, document.title, cleanUrl);
|
|
11643
|
+
}
|
|
11644
|
+
else {
|
|
11645
|
+
setError(errorMessage);
|
|
11646
|
+
}
|
|
11647
|
+
onAuthError?.(err instanceof Error ? err : new Error('An error occurred'));
|
|
10890
11648
|
}
|
|
10891
11649
|
finally {
|
|
10892
11650
|
setLoading(false);
|
|
10893
11651
|
}
|
|
10894
11652
|
};
|
|
10895
|
-
const
|
|
10896
|
-
e.preventDefault();
|
|
11653
|
+
const handleEmailAuth = async (data) => {
|
|
10897
11654
|
setLoading(true);
|
|
10898
11655
|
setError(undefined);
|
|
10899
|
-
|
|
11656
|
+
setAuthSuccess(false);
|
|
10900
11657
|
try {
|
|
10901
|
-
|
|
10902
|
-
|
|
10903
|
-
|
|
10904
|
-
|
|
10905
|
-
|
|
10906
|
-
|
|
10907
|
-
|
|
10908
|
-
//
|
|
10909
|
-
|
|
10910
|
-
|
|
10911
|
-
|
|
10912
|
-
|
|
10913
|
-
|
|
10914
|
-
|
|
10915
|
-
|
|
10916
|
-
|
|
11658
|
+
const response = mode === 'login'
|
|
11659
|
+
? await api.login(data.email, data.password)
|
|
11660
|
+
: await api.register({
|
|
11661
|
+
...data,
|
|
11662
|
+
accountData: mode === 'register' ? accountData : undefined,
|
|
11663
|
+
redirectUrl: getRedirectUrl(), // Include redirect URL for email verification
|
|
11664
|
+
});
|
|
11665
|
+
// Get email verification mode from response or config (default: verify-then-auto-login)
|
|
11666
|
+
const verificationMode = response.emailVerificationMode || config?.emailVerification?.mode || 'verify-then-auto-login';
|
|
11667
|
+
const gracePeriodHours = config?.emailVerification?.gracePeriodHours || 24;
|
|
11668
|
+
if (mode === 'register') {
|
|
11669
|
+
// Handle different verification modes
|
|
11670
|
+
if (verificationMode === 'immediate' && response.token) {
|
|
11671
|
+
// Immediate mode: Log in right away if token is provided
|
|
11672
|
+
auth.login(response.token, response.user, response.accountData);
|
|
11673
|
+
setAuthSuccess(true);
|
|
11674
|
+
const deadline = response.emailVerificationDeadline
|
|
11675
|
+
? new Date(response.emailVerificationDeadline).toLocaleString()
|
|
11676
|
+
: `${gracePeriodHours} hours`;
|
|
11677
|
+
setSuccessMessage(`Account created! You're logged in now. Please verify your email by ${deadline} to keep your account active.`);
|
|
11678
|
+
if (response.token) {
|
|
11679
|
+
onAuthSuccess(response.token, response.user, response.accountData);
|
|
11680
|
+
}
|
|
11681
|
+
if (redirectUrl) {
|
|
11682
|
+
setTimeout(() => {
|
|
11683
|
+
window.location.href = redirectUrl;
|
|
11684
|
+
}, 2000);
|
|
11685
|
+
}
|
|
11686
|
+
}
|
|
11687
|
+
else if (verificationMode === 'verify-then-auto-login') {
|
|
11688
|
+
// Verify-then-auto-login mode: Don't log in yet, but will auto-login after email verification
|
|
11689
|
+
setAuthSuccess(true);
|
|
11690
|
+
setSuccessMessage('Account created! Please check your email and click the verification link to complete your registration.');
|
|
11691
|
+
}
|
|
11692
|
+
else {
|
|
11693
|
+
// verify-then-manual-login mode: Traditional flow
|
|
11694
|
+
setAuthSuccess(true);
|
|
11695
|
+
setSuccessMessage('Account created successfully! Please check your email to verify your account, then log in.');
|
|
11696
|
+
}
|
|
11697
|
+
}
|
|
11698
|
+
else {
|
|
11699
|
+
// Login mode - always log in if token is provided
|
|
11700
|
+
if (response.token) {
|
|
11701
|
+
// Check for account lock or verification requirements
|
|
11702
|
+
if (response.accountLocked) {
|
|
11703
|
+
throw new Error('Your account has been locked due to unverified email. Please check your email or request a new verification link.');
|
|
11704
|
+
}
|
|
11705
|
+
if (response.requiresEmailVerification) {
|
|
11706
|
+
throw new Error('Please verify your email before logging in. Check your inbox for the verification link.');
|
|
11707
|
+
}
|
|
11708
|
+
auth.login(response.token, response.user, response.accountData);
|
|
11709
|
+
setAuthSuccess(true);
|
|
11710
|
+
setSuccessMessage('Login successful!');
|
|
11711
|
+
onAuthSuccess(response.token, response.user, response.accountData);
|
|
11712
|
+
if (redirectUrl) {
|
|
11713
|
+
setTimeout(() => {
|
|
11714
|
+
window.location.href = redirectUrl;
|
|
11715
|
+
}, 2000);
|
|
11716
|
+
}
|
|
11717
|
+
}
|
|
11718
|
+
else {
|
|
11719
|
+
throw new Error('Authentication failed - please verify your email before logging in.');
|
|
11720
|
+
}
|
|
11721
|
+
}
|
|
10917
11722
|
}
|
|
10918
11723
|
catch (err) {
|
|
10919
|
-
const errorMessage = err instanceof Error ? err.message : '
|
|
10920
|
-
|
|
10921
|
-
|
|
11724
|
+
const errorMessage = err instanceof Error ? err.message : 'Authentication failed';
|
|
11725
|
+
// Check if error is about email already registered
|
|
11726
|
+
if (mode === 'register' && errorMessage.toLowerCase().includes('already') && errorMessage.toLowerCase().includes('email')) {
|
|
11727
|
+
setShowResendVerification(true);
|
|
11728
|
+
setResendEmail(data.email);
|
|
11729
|
+
setError('This email is already registered. If you didn\'t receive the verification email, you can resend it below.');
|
|
11730
|
+
}
|
|
11731
|
+
else {
|
|
11732
|
+
setError(errorMessage);
|
|
11733
|
+
}
|
|
11734
|
+
onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
10922
11735
|
}
|
|
10923
11736
|
finally {
|
|
10924
11737
|
setLoading(false);
|
|
10925
11738
|
}
|
|
10926
11739
|
};
|
|
10927
|
-
const
|
|
10928
|
-
|
|
10929
|
-
|
|
10930
|
-
setNewEmail('');
|
|
10931
|
-
setEmailPassword('');
|
|
10932
|
-
setCurrentPassword('');
|
|
10933
|
-
setNewPassword('');
|
|
10934
|
-
setConfirmPassword('');
|
|
10935
|
-
setNewPhone('');
|
|
10936
|
-
setPhoneCode('');
|
|
10937
|
-
setPhoneCodeSent(false);
|
|
10938
|
-
setError(undefined);
|
|
10939
|
-
setSuccess(undefined);
|
|
10940
|
-
};
|
|
10941
|
-
const handleChangeEmail = async (e) => {
|
|
10942
|
-
e.preventDefault();
|
|
11740
|
+
const handleResendVerification = async () => {
|
|
11741
|
+
if (!resendEmail)
|
|
11742
|
+
return;
|
|
10943
11743
|
setLoading(true);
|
|
10944
11744
|
setError(undefined);
|
|
10945
|
-
setSuccess(undefined);
|
|
10946
11745
|
try {
|
|
10947
|
-
//
|
|
10948
|
-
//
|
|
10949
|
-
|
|
10950
|
-
|
|
10951
|
-
|
|
10952
|
-
|
|
10953
|
-
console.log('Data:', { newEmail });
|
|
10954
|
-
// Uncomment when backend is ready:
|
|
10955
|
-
// await smartlinks.authKit.changeEmail(clientId, newEmail, emailPassword);
|
|
10956
|
-
// setSuccess('Email changed successfully!');
|
|
10957
|
-
// setEditingSection(null);
|
|
10958
|
-
// setNewEmail('');
|
|
10959
|
-
// setEmailPassword('');
|
|
10960
|
-
// onEmailChangeRequested?.();
|
|
10961
|
-
// await loadProfile(); // Reload to show new email
|
|
11746
|
+
// For resend, we need the userId. If we don't have it, we need to handle this differently
|
|
11747
|
+
// The backend should ideally handle this case
|
|
11748
|
+
await api.resendVerification('unknown', resendEmail, getRedirectUrl());
|
|
11749
|
+
setAuthSuccess(true);
|
|
11750
|
+
setSuccessMessage('Verification email sent! Please check your inbox.');
|
|
11751
|
+
setShowResendVerification(false);
|
|
10962
11752
|
}
|
|
10963
11753
|
catch (err) {
|
|
10964
|
-
const errorMessage = err instanceof Error ? err.message : 'Failed to
|
|
11754
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to resend verification email';
|
|
10965
11755
|
setError(errorMessage);
|
|
10966
|
-
|
|
11756
|
+
onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
10967
11757
|
}
|
|
10968
11758
|
finally {
|
|
10969
11759
|
setLoading(false);
|
|
10970
11760
|
}
|
|
10971
11761
|
};
|
|
10972
|
-
const
|
|
10973
|
-
|
|
10974
|
-
if (newPassword !== confirmPassword) {
|
|
10975
|
-
setError('New passwords do not match');
|
|
10976
|
-
return;
|
|
10977
|
-
}
|
|
10978
|
-
if (newPassword.length < 6) {
|
|
10979
|
-
setError('Password must be at least 6 characters');
|
|
11762
|
+
const handleRequestNewReset = async () => {
|
|
11763
|
+
if (!resetRequestEmail)
|
|
10980
11764
|
return;
|
|
10981
|
-
}
|
|
10982
11765
|
setLoading(true);
|
|
10983
11766
|
setError(undefined);
|
|
10984
|
-
setSuccess(undefined);
|
|
10985
11767
|
try {
|
|
10986
|
-
|
|
10987
|
-
|
|
10988
|
-
|
|
10989
|
-
|
|
10990
|
-
|
|
10991
|
-
console.log('Data: currentPassword and newPassword provided');
|
|
10992
|
-
// Uncomment when backend is ready:
|
|
10993
|
-
// await smartlinks.authKit.changePassword(clientId, currentPassword, newPassword);
|
|
10994
|
-
// setSuccess('Password changed successfully!');
|
|
10995
|
-
// setEditingSection(null);
|
|
10996
|
-
// setCurrentPassword('');
|
|
10997
|
-
// setNewPassword('');
|
|
10998
|
-
// setConfirmPassword('');
|
|
10999
|
-
// onPasswordChanged?.();
|
|
11768
|
+
await api.requestPasswordReset(resetRequestEmail, getRedirectUrl());
|
|
11769
|
+
setAuthSuccess(true);
|
|
11770
|
+
setSuccessMessage('Password reset email sent! Please check your inbox.');
|
|
11771
|
+
setShowRequestNewReset(false);
|
|
11772
|
+
setResetRequestEmail('');
|
|
11000
11773
|
}
|
|
11001
11774
|
catch (err) {
|
|
11002
|
-
const errorMessage = err instanceof Error ? err.message : 'Failed to
|
|
11775
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to send password reset email';
|
|
11003
11776
|
setError(errorMessage);
|
|
11004
|
-
|
|
11777
|
+
onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
11005
11778
|
}
|
|
11006
11779
|
finally {
|
|
11007
11780
|
setLoading(false);
|
|
11008
11781
|
}
|
|
11009
11782
|
};
|
|
11010
|
-
const
|
|
11783
|
+
const handleGoogleLogin = async () => {
|
|
11784
|
+
// Use custom client ID from config, or fall back to default Smartlinks client ID
|
|
11785
|
+
const googleClientId = config?.googleClientId || DEFAULT_GOOGLE_CLIENT_ID;
|
|
11786
|
+
// Determine OAuth flow: default to 'oneTap' for better UX, but allow 'popup' for iframe compatibility
|
|
11787
|
+
const oauthFlow = config?.googleOAuthFlow || 'oneTap';
|
|
11011
11788
|
setLoading(true);
|
|
11012
11789
|
setError(undefined);
|
|
11013
11790
|
try {
|
|
11014
|
-
|
|
11015
|
-
|
|
11016
|
-
|
|
11791
|
+
const google = window.google;
|
|
11792
|
+
if (!google) {
|
|
11793
|
+
throw new Error('Google Identity Services not loaded. Please check your internet connection.');
|
|
11794
|
+
}
|
|
11795
|
+
if (oauthFlow === 'popup') {
|
|
11796
|
+
// Use OAuth2 popup flow (works in iframes but requires popup permission)
|
|
11797
|
+
if (!google.accounts.oauth2) {
|
|
11798
|
+
throw new Error('Google OAuth2 not available');
|
|
11799
|
+
}
|
|
11800
|
+
const client = google.accounts.oauth2.initTokenClient({
|
|
11801
|
+
client_id: googleClientId,
|
|
11802
|
+
scope: 'openid email profile',
|
|
11803
|
+
callback: async (response) => {
|
|
11804
|
+
try {
|
|
11805
|
+
if (response.error) {
|
|
11806
|
+
throw new Error(response.error_description || response.error);
|
|
11807
|
+
}
|
|
11808
|
+
const accessToken = response.access_token;
|
|
11809
|
+
// Send access token to backend
|
|
11810
|
+
const authResponse = await api.loginWithGoogle(accessToken);
|
|
11811
|
+
if (authResponse.token) {
|
|
11812
|
+
auth.login(authResponse.token, authResponse.user, authResponse.accountData);
|
|
11813
|
+
setAuthSuccess(true);
|
|
11814
|
+
setSuccessMessage('Google login successful!');
|
|
11815
|
+
onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
|
|
11816
|
+
}
|
|
11817
|
+
else {
|
|
11818
|
+
throw new Error('Authentication failed - no token received');
|
|
11819
|
+
}
|
|
11820
|
+
if (redirectUrl) {
|
|
11821
|
+
setTimeout(() => {
|
|
11822
|
+
window.location.href = redirectUrl;
|
|
11823
|
+
}, 2000);
|
|
11824
|
+
}
|
|
11825
|
+
setLoading(false);
|
|
11826
|
+
}
|
|
11827
|
+
catch (err) {
|
|
11828
|
+
const errorMessage = err instanceof Error ? err.message : 'Google login failed';
|
|
11829
|
+
setError(errorMessage);
|
|
11830
|
+
onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
11831
|
+
setLoading(false);
|
|
11832
|
+
}
|
|
11833
|
+
},
|
|
11834
|
+
});
|
|
11835
|
+
client.requestAccessToken();
|
|
11836
|
+
}
|
|
11837
|
+
else {
|
|
11838
|
+
// Use One Tap / Sign-In button flow (smoother UX but doesn't work in iframes)
|
|
11839
|
+
google.accounts.id.initialize({
|
|
11840
|
+
client_id: googleClientId,
|
|
11841
|
+
callback: async (response) => {
|
|
11842
|
+
try {
|
|
11843
|
+
const idToken = response.credential;
|
|
11844
|
+
const authResponse = await api.loginWithGoogle(idToken);
|
|
11845
|
+
if (authResponse.token) {
|
|
11846
|
+
auth.login(authResponse.token, authResponse.user, authResponse.accountData);
|
|
11847
|
+
setAuthSuccess(true);
|
|
11848
|
+
setSuccessMessage('Google login successful!');
|
|
11849
|
+
onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
|
|
11850
|
+
}
|
|
11851
|
+
else {
|
|
11852
|
+
throw new Error('Authentication failed - no token received');
|
|
11853
|
+
}
|
|
11854
|
+
if (redirectUrl) {
|
|
11855
|
+
setTimeout(() => {
|
|
11856
|
+
window.location.href = redirectUrl;
|
|
11857
|
+
}, 2000);
|
|
11858
|
+
}
|
|
11859
|
+
setLoading(false);
|
|
11860
|
+
}
|
|
11861
|
+
catch (err) {
|
|
11862
|
+
const errorMessage = err instanceof Error ? err.message : 'Google login failed';
|
|
11863
|
+
setError(errorMessage);
|
|
11864
|
+
onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
11865
|
+
setLoading(false);
|
|
11866
|
+
}
|
|
11867
|
+
},
|
|
11868
|
+
auto_select: false,
|
|
11869
|
+
cancel_on_tap_outside: true,
|
|
11870
|
+
});
|
|
11871
|
+
google.accounts.id.prompt((notification) => {
|
|
11872
|
+
if (notification.isNotDisplayed() || notification.isSkippedMoment()) {
|
|
11873
|
+
setLoading(false);
|
|
11874
|
+
}
|
|
11875
|
+
});
|
|
11876
|
+
}
|
|
11017
11877
|
}
|
|
11018
11878
|
catch (err) {
|
|
11019
|
-
const errorMessage = err instanceof Error ? err.message : '
|
|
11879
|
+
const errorMessage = err instanceof Error ? err.message : 'Google login failed';
|
|
11020
11880
|
setError(errorMessage);
|
|
11021
|
-
|
|
11022
|
-
}
|
|
11023
|
-
finally {
|
|
11881
|
+
onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
11024
11882
|
setLoading(false);
|
|
11025
11883
|
}
|
|
11026
11884
|
};
|
|
11027
|
-
const
|
|
11028
|
-
e.preventDefault();
|
|
11885
|
+
const handlePhoneAuth = async (phoneNumber, verificationCode) => {
|
|
11029
11886
|
setLoading(true);
|
|
11030
11887
|
setError(undefined);
|
|
11031
|
-
setSuccess(undefined);
|
|
11032
11888
|
try {
|
|
11033
|
-
|
|
11034
|
-
|
|
11035
|
-
|
|
11036
|
-
|
|
11037
|
-
|
|
11038
|
-
|
|
11039
|
-
|
|
11040
|
-
|
|
11041
|
-
|
|
11042
|
-
|
|
11043
|
-
|
|
11044
|
-
|
|
11045
|
-
|
|
11046
|
-
|
|
11889
|
+
if (!verificationCode) {
|
|
11890
|
+
// Send verification code via Twilio Verify Service
|
|
11891
|
+
await api.sendPhoneCode(phoneNumber);
|
|
11892
|
+
// Twilio Verify Service tracks the verification by phone number
|
|
11893
|
+
// No need to store verificationId
|
|
11894
|
+
}
|
|
11895
|
+
else {
|
|
11896
|
+
// Verify code - Twilio identifies the verification by phone number
|
|
11897
|
+
const response = await api.verifyPhoneCode(phoneNumber, verificationCode);
|
|
11898
|
+
// Update auth context with account data if token is provided
|
|
11899
|
+
if (response.token) {
|
|
11900
|
+
auth.login(response.token, response.user, response.accountData);
|
|
11901
|
+
onAuthSuccess(response.token, response.user, response.accountData);
|
|
11902
|
+
if (redirectUrl) {
|
|
11903
|
+
window.location.href = redirectUrl;
|
|
11904
|
+
}
|
|
11905
|
+
}
|
|
11906
|
+
else {
|
|
11907
|
+
throw new Error('Authentication failed - no token received');
|
|
11908
|
+
}
|
|
11909
|
+
}
|
|
11047
11910
|
}
|
|
11048
11911
|
catch (err) {
|
|
11049
|
-
const errorMessage = err instanceof Error ? err.message : '
|
|
11912
|
+
const errorMessage = err instanceof Error ? err.message : 'Phone authentication failed';
|
|
11050
11913
|
setError(errorMessage);
|
|
11051
|
-
|
|
11914
|
+
onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
11052
11915
|
}
|
|
11053
11916
|
finally {
|
|
11054
11917
|
setLoading(false);
|
|
11055
11918
|
}
|
|
11056
11919
|
};
|
|
11057
|
-
const
|
|
11058
|
-
|
|
11059
|
-
|
|
11060
|
-
|
|
11920
|
+
const handlePasswordReset = async (emailOrPassword, confirmPassword) => {
|
|
11921
|
+
setLoading(true);
|
|
11922
|
+
setError(undefined);
|
|
11923
|
+
try {
|
|
11924
|
+
if (resetToken && confirmPassword) {
|
|
11925
|
+
// Complete password reset with token
|
|
11926
|
+
await api.completePasswordReset(resetToken, emailOrPassword);
|
|
11927
|
+
setResetSuccess(true);
|
|
11928
|
+
setResetToken(undefined); // Clear token after successful reset
|
|
11929
|
+
}
|
|
11930
|
+
else {
|
|
11931
|
+
// Request password reset email
|
|
11932
|
+
await api.requestPasswordReset(emailOrPassword, getRedirectUrl());
|
|
11933
|
+
setResetSuccess(true);
|
|
11934
|
+
}
|
|
11061
11935
|
}
|
|
11062
|
-
|
|
11063
|
-
|
|
11064
|
-
|
|
11936
|
+
catch (err) {
|
|
11937
|
+
const errorMessage = err instanceof Error ? err.message : 'Password reset failed';
|
|
11938
|
+
setError(errorMessage);
|
|
11939
|
+
onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
11940
|
+
}
|
|
11941
|
+
finally {
|
|
11942
|
+
setLoading(false);
|
|
11065
11943
|
}
|
|
11944
|
+
};
|
|
11945
|
+
const handleMagicLink = async (email) => {
|
|
11066
11946
|
setLoading(true);
|
|
11067
11947
|
setError(undefined);
|
|
11068
11948
|
try {
|
|
11069
|
-
|
|
11070
|
-
|
|
11071
|
-
|
|
11072
|
-
// Note: This performs a SOFT DELETE (marks as deleted, obfuscates email)
|
|
11073
|
-
setError('Backend API not yet implemented. See console for required endpoint.');
|
|
11074
|
-
console.log('Required API endpoint: DELETE /api/v1/authkit/:clientId/account/delete');
|
|
11075
|
-
console.log('Data: password and confirmText="DELETE" provided');
|
|
11076
|
-
console.log('Note: Backend should soft delete (mark deleted, obfuscate email, disable account)');
|
|
11077
|
-
// Uncomment when backend is ready:
|
|
11078
|
-
// await smartlinks.authKit.deleteAccount(clientId, deletePassword, deleteConfirmText);
|
|
11079
|
-
// setSuccess('Account deleted successfully');
|
|
11080
|
-
// onAccountDeleted?.();
|
|
11081
|
-
// await auth.logout();
|
|
11949
|
+
await api.sendMagicLink(email, getRedirectUrl());
|
|
11950
|
+
setAuthSuccess(true);
|
|
11951
|
+
setSuccessMessage('Magic link sent! Check your email to log in.');
|
|
11082
11952
|
}
|
|
11083
11953
|
catch (err) {
|
|
11084
|
-
const errorMessage = err instanceof Error ? err.message : 'Failed to
|
|
11954
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to send magic link';
|
|
11085
11955
|
setError(errorMessage);
|
|
11086
|
-
|
|
11956
|
+
onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
11087
11957
|
}
|
|
11088
11958
|
finally {
|
|
11089
11959
|
setLoading(false);
|
|
11090
11960
|
}
|
|
11091
11961
|
};
|
|
11092
|
-
if (
|
|
11093
|
-
return (jsx(
|
|
11094
|
-
}
|
|
11095
|
-
if (loading && !profile) {
|
|
11096
|
-
return (jsx("div", { className: `account-management ${className}`, children: jsx("p", { className: "text-muted-foreground", children: "Loading..." }) }));
|
|
11097
|
-
}
|
|
11098
|
-
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" })] })] }))] })), 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: () => {
|
|
11099
|
-
setShowDeleteConfirm(false);
|
|
11100
|
-
setDeletePassword('');
|
|
11101
|
-
setDeleteConfirmText('');
|
|
11102
|
-
}, className: "auth-button button-secondary", children: "Cancel" })] })] }))] }))] }));
|
|
11103
|
-
};
|
|
11104
|
-
|
|
11105
|
-
const ProtectedRoute = ({ children, fallback, redirectTo, }) => {
|
|
11106
|
-
const { isAuthenticated, isLoading } = useAuth();
|
|
11107
|
-
// Show loading state
|
|
11108
|
-
if (isLoading) {
|
|
11109
|
-
return jsx("div", { children: "Loading..." });
|
|
11110
|
-
}
|
|
11111
|
-
// If not authenticated, redirect or show fallback
|
|
11112
|
-
if (!isAuthenticated) {
|
|
11113
|
-
if (redirectTo) {
|
|
11114
|
-
window.location.href = redirectTo;
|
|
11115
|
-
return null;
|
|
11116
|
-
}
|
|
11117
|
-
return fallback ? jsx(Fragment, { children: fallback }) : jsx("div", { children: "Access denied. Please log in." });
|
|
11962
|
+
if (configLoading) {
|
|
11963
|
+
return (jsx(AuthContainer, { theme: theme, className: className, minimal: minimal || config?.branding?.minimal || false, children: jsx("div", { style: { textAlign: 'center', padding: '2rem' }, children: jsx("div", { className: "auth-spinner" }) }) }));
|
|
11118
11964
|
}
|
|
11119
|
-
|
|
11120
|
-
|
|
11965
|
+
return (jsx(AuthContainer, { theme: theme, className: className, config: config, minimal: minimal || config?.branding?.minimal || false, children: authSuccess ? (jsxs("div", { style: { textAlign: 'center', padding: '2rem' }, children: [jsx("div", { style: {
|
|
11966
|
+
color: 'var(--auth-primary-color, #4F46E5)',
|
|
11967
|
+
fontSize: '3rem',
|
|
11968
|
+
marginBottom: '1rem'
|
|
11969
|
+
}, children: "\u2713" }), jsx("h2", { style: {
|
|
11970
|
+
marginBottom: '0.5rem',
|
|
11971
|
+
fontSize: '1.5rem',
|
|
11972
|
+
fontWeight: 600
|
|
11973
|
+
}, children: successMessage?.includes('verified') ? 'Email Verified!' :
|
|
11974
|
+
successMessage?.includes('Magic link') ? 'Check Your Email!' :
|
|
11975
|
+
mode === 'register' ? 'Account Created!' : 'Login Successful!' }), jsx("p", { style: {
|
|
11976
|
+
color: '#6B7280',
|
|
11977
|
+
fontSize: '0.875rem'
|
|
11978
|
+
}, children: successMessage })] })) : mode === 'magic-link' ? (jsx(MagicLinkForm, { onSubmit: handleMagicLink, onCancel: () => setMode('login'), loading: loading, error: error })) : mode === 'phone' ? (jsx(PhoneAuthForm, { onSubmit: handlePhoneAuth, onBack: () => setMode('login'), loading: loading, error: error })) : mode === 'reset-password' ? (jsx(PasswordResetForm, { onSubmit: handlePasswordReset, onBack: () => {
|
|
11979
|
+
setMode('login');
|
|
11980
|
+
setResetSuccess(false);
|
|
11981
|
+
setResetToken(undefined); // Clear token when going back
|
|
11982
|
+
}, loading: loading, error: error, success: resetSuccess, token: resetToken })) : (mode === 'login' || mode === 'register') ? (jsx(Fragment, { children: showResendVerification ? (jsxs("div", { style: { marginTop: '1rem', padding: '1.5rem', backgroundColor: 'rgba(79, 70, 229, 0.05)', borderRadius: '0.5rem' }, children: [jsx("h3", { style: { marginBottom: '0.75rem', fontSize: '1rem', fontWeight: 600, color: 'var(--auth-text-color, #374151)' }, children: "Verification Link Expired" }), jsx("p", { style: { marginBottom: '1rem', fontSize: '0.875rem', color: 'var(--auth-text-color, #6B7280)', lineHeight: '1.5' }, children: "Your verification link has expired or is no longer valid. Please enter your email address below and we'll send you a new verification link." }), jsx("input", { type: "email", value: resendEmail || '', onChange: (e) => setResendEmail(e.target.value), placeholder: "your@email.com", style: {
|
|
11983
|
+
width: '100%',
|
|
11984
|
+
padding: '0.625rem',
|
|
11985
|
+
marginBottom: '1rem',
|
|
11986
|
+
border: '1px solid var(--auth-border-color, #D1D5DB)',
|
|
11987
|
+
borderRadius: '0.375rem',
|
|
11988
|
+
fontSize: '0.875rem',
|
|
11989
|
+
boxSizing: 'border-box'
|
|
11990
|
+
} }), jsxs("div", { style: { display: 'flex', gap: '0.75rem' }, children: [jsx("button", { onClick: handleResendVerification, disabled: loading || !resendEmail, style: {
|
|
11991
|
+
flex: 1,
|
|
11992
|
+
padding: '0.625rem 1rem',
|
|
11993
|
+
backgroundColor: 'var(--auth-primary-color, #4F46E5)',
|
|
11994
|
+
color: 'white',
|
|
11995
|
+
border: 'none',
|
|
11996
|
+
borderRadius: '0.375rem',
|
|
11997
|
+
cursor: (loading || !resendEmail) ? 'not-allowed' : 'pointer',
|
|
11998
|
+
fontSize: '0.875rem',
|
|
11999
|
+
fontWeight: 500,
|
|
12000
|
+
opacity: (loading || !resendEmail) ? 0.6 : 1
|
|
12001
|
+
}, children: loading ? 'Sending...' : 'Send New Verification Link' }), jsx("button", { onClick: () => {
|
|
12002
|
+
setShowResendVerification(false);
|
|
12003
|
+
setResendEmail('');
|
|
12004
|
+
setError(undefined);
|
|
12005
|
+
}, disabled: loading, style: {
|
|
12006
|
+
padding: '0.625rem 1rem',
|
|
12007
|
+
backgroundColor: 'transparent',
|
|
12008
|
+
color: 'var(--auth-text-color, #6B7280)',
|
|
12009
|
+
border: '1px solid var(--auth-border-color, #D1D5DB)',
|
|
12010
|
+
borderRadius: '0.375rem',
|
|
12011
|
+
cursor: loading ? 'not-allowed' : 'pointer',
|
|
12012
|
+
fontSize: '0.875rem',
|
|
12013
|
+
fontWeight: 500,
|
|
12014
|
+
opacity: loading ? 0.6 : 1
|
|
12015
|
+
}, children: "Cancel" })] })] })) : showRequestNewReset ? (jsxs("div", { style: { marginTop: '1rem', padding: '1.5rem', backgroundColor: 'rgba(239, 68, 68, 0.05)', borderRadius: '0.5rem' }, children: [jsx("h3", { style: { marginBottom: '0.75rem', fontSize: '1rem', fontWeight: 600, color: 'var(--auth-text-color, #374151)' }, children: "Password Reset Link Expired" }), jsx("p", { style: { marginBottom: '1rem', fontSize: '0.875rem', color: 'var(--auth-text-color, #6B7280)', lineHeight: '1.5' }, children: "Your password reset link has expired or is no longer valid. Please enter your email address below and we'll send you a new password reset link." }), jsx("input", { type: "email", value: resetRequestEmail || '', onChange: (e) => setResetRequestEmail(e.target.value), placeholder: "your@email.com", style: {
|
|
12016
|
+
width: '100%',
|
|
12017
|
+
padding: '0.625rem',
|
|
12018
|
+
marginBottom: '1rem',
|
|
12019
|
+
border: '1px solid var(--auth-border-color, #D1D5DB)',
|
|
12020
|
+
borderRadius: '0.375rem',
|
|
12021
|
+
fontSize: '0.875rem',
|
|
12022
|
+
boxSizing: 'border-box'
|
|
12023
|
+
} }), jsxs("div", { style: { display: 'flex', gap: '0.75rem' }, children: [jsx("button", { onClick: handleRequestNewReset, disabled: loading || !resetRequestEmail, style: {
|
|
12024
|
+
flex: 1,
|
|
12025
|
+
padding: '0.625rem 1rem',
|
|
12026
|
+
backgroundColor: '#EF4444',
|
|
12027
|
+
color: 'white',
|
|
12028
|
+
border: 'none',
|
|
12029
|
+
borderRadius: '0.375rem',
|
|
12030
|
+
cursor: (loading || !resetRequestEmail) ? 'not-allowed' : 'pointer',
|
|
12031
|
+
fontSize: '0.875rem',
|
|
12032
|
+
fontWeight: 500,
|
|
12033
|
+
opacity: (loading || !resetRequestEmail) ? 0.6 : 1
|
|
12034
|
+
}, children: loading ? 'Sending...' : 'Send New Reset Link' }), jsx("button", { onClick: () => {
|
|
12035
|
+
setShowRequestNewReset(false);
|
|
12036
|
+
setResetRequestEmail('');
|
|
12037
|
+
setError(undefined);
|
|
12038
|
+
}, disabled: loading, style: {
|
|
12039
|
+
padding: '0.625rem 1rem',
|
|
12040
|
+
backgroundColor: 'transparent',
|
|
12041
|
+
color: 'var(--auth-text-color, #6B7280)',
|
|
12042
|
+
border: '1px solid var(--auth-border-color, #D1D5DB)',
|
|
12043
|
+
borderRadius: '0.375rem',
|
|
12044
|
+
cursor: loading ? 'not-allowed' : 'pointer',
|
|
12045
|
+
fontSize: '0.875rem',
|
|
12046
|
+
fontWeight: 500,
|
|
12047
|
+
opacity: loading ? 0.6 : 1
|
|
12048
|
+
}, children: "Cancel" })] })] })) : (jsx(Fragment, { children: (() => {
|
|
12049
|
+
const emailDisplayMode = config?.emailDisplayMode || 'form';
|
|
12050
|
+
const providerOrder = config?.providerOrder || (config?.enabledProviders || enabledProviders);
|
|
12051
|
+
const actualProviders = config?.enabledProviders || enabledProviders;
|
|
12052
|
+
// Button mode: show provider selection first, then email form if email is selected
|
|
12053
|
+
if (emailDisplayMode === 'button' && !showEmailForm) {
|
|
12054
|
+
return (jsx(ProviderButtons, { enabledProviders: actualProviders, providerOrder: providerOrder, onEmailLogin: () => setShowEmailForm(true), onGoogleLogin: handleGoogleLogin, onPhoneLogin: () => setMode('phone'), onMagicLinkLogin: () => setMode('magic-link'), loading: loading }));
|
|
12055
|
+
}
|
|
12056
|
+
// Form mode or email button was clicked: show email form with other providers
|
|
12057
|
+
return (jsxs(Fragment, { children: [emailDisplayMode === 'button' && showEmailForm && (jsx("button", { onClick: () => setShowEmailForm(false), style: {
|
|
12058
|
+
marginBottom: '1rem',
|
|
12059
|
+
padding: '0.5rem',
|
|
12060
|
+
background: 'none',
|
|
12061
|
+
border: 'none',
|
|
12062
|
+
color: 'var(--auth-text-color, #6B7280)',
|
|
12063
|
+
cursor: 'pointer',
|
|
12064
|
+
fontSize: '0.875rem',
|
|
12065
|
+
display: 'flex',
|
|
12066
|
+
alignItems: 'center',
|
|
12067
|
+
gap: '0.25rem'
|
|
12068
|
+
}, children: "\u2190 Back to options" })), jsx(EmailAuthForm, { mode: mode, onSubmit: handleEmailAuth, onModeSwitch: () => {
|
|
12069
|
+
setMode(mode === 'login' ? 'register' : 'login');
|
|
12070
|
+
setShowResendVerification(false);
|
|
12071
|
+
setShowRequestNewReset(false);
|
|
12072
|
+
setError(undefined);
|
|
12073
|
+
}, onForgotPassword: () => setMode('reset-password'), loading: loading, error: error, 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'), loading: loading }))] }));
|
|
12074
|
+
})() })) })) : null }));
|
|
11121
12075
|
};
|
|
11122
12076
|
|
|
11123
|
-
const
|
|
11124
|
-
const
|
|
11125
|
-
const showGoogle = enabledProviders.includes('google');
|
|
11126
|
-
const showPhone = enabledProviders.includes('phone');
|
|
11127
|
-
const showMagicLink = enabledProviders.includes('magic-link');
|
|
11128
|
-
// Determine ordered providers (excluding email if in button mode)
|
|
11129
|
-
const orderedProviders = providerOrder && providerOrder.length > 0
|
|
11130
|
-
? providerOrder.filter(p => enabledProviders.includes(p) && p !== 'email')
|
|
11131
|
-
: enabledProviders.filter(p => p !== 'email');
|
|
11132
|
-
const hasOtherProviders = showGoogle || showPhone || showMagicLink;
|
|
11133
|
-
// Render provider button helper
|
|
11134
|
-
const renderProviderButton = (provider) => {
|
|
11135
|
-
if (provider === 'google' && showGoogle) {
|
|
11136
|
-
return (jsxs("button", { className: "auth-provider-button", disabled: true, children: [jsxs("svg", { width: "18", height: "18", viewBox: "0 0 18 18", fill: "none", children: [jsx("path", { d: "M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844c-.209 1.125-.843 2.078-1.796 2.717v2.258h2.908c1.702-1.567 2.684-3.874 2.684-6.615z", fill: "#4285F4" }), jsx("path", { d: "M9 18c2.43 0 4.467-.806 5.956-2.183l-2.908-2.259c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332C2.438 15.983 5.482 18 9 18z", fill: "#34A853" }), jsx("path", { d: "M3.964 10.707c-.18-.54-.282-1.117-.282-1.707 0-.593.102-1.167.282-1.707V4.961H.957C.347 6.175 0 7.548 0 9s.348 2.825.957 4.039l3.007-2.332z", fill: "#FBBC05" }), jsx("path", { d: "M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0 5.482 0 2.438 2.017.957 4.958L3.964 7.29C4.672 5.163 6.656 3.58 9 3.58z", fill: "#EA4335" })] }), jsx("span", { children: "Continue with Google" })] }, "google"));
|
|
11137
|
-
}
|
|
11138
|
-
if (provider === 'phone' && showPhone) {
|
|
11139
|
-
return (jsxs("button", { className: "auth-provider-button", disabled: true, children: [jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: jsx("path", { d: "M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z" }) }), jsx("span", { children: "Continue with Phone" })] }, "phone"));
|
|
11140
|
-
}
|
|
11141
|
-
if (provider === 'magic-link' && showMagicLink) {
|
|
11142
|
-
return (jsxs("button", { className: "auth-provider-button", disabled: true, children: [jsx("svg", { width: "18", height: "18", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" }) }), jsx("span", { children: "Continue with Magic Link" })] }, "magic-link"));
|
|
11143
|
-
}
|
|
11144
|
-
if (provider === 'email' && showEmail && emailDisplayMode === 'button') {
|
|
11145
|
-
return (jsxs("button", { className: "auth-provider-button", disabled: true, children: [jsx("svg", { width: "18", height: "18", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" }) }), jsx("span", { children: "Continue with Email" })] }, "email"));
|
|
11146
|
-
}
|
|
11147
|
-
return null;
|
|
11148
|
-
};
|
|
11149
|
-
return (jsx(AuthContainer, { theme: theme, className: className, config: customization, children: emailDisplayMode === 'button' ? (jsx("div", { className: "auth-provider-buttons", children: orderedProviders.concat(showEmail ? ['email'] : []).map(provider => renderProviderButton(provider)) })) : (
|
|
11150
|
-
/* Form mode: show email form first, then other providers */
|
|
11151
|
-
jsxs(Fragment, { children: [showEmail && (jsxs("div", { className: "auth-form", children: [jsxs("div", { className: "auth-form-group", children: [jsx("label", { className: "auth-label", children: "Email" }), jsx("input", { type: "email", className: "auth-input", placeholder: "Enter your email", disabled: true })] }), jsxs("div", { className: "auth-form-group", children: [jsx("label", { className: "auth-label", children: "Password" }), jsx("input", { type: "password", className: "auth-input", placeholder: "Enter your password", disabled: true })] }), jsx("button", { className: "auth-button auth-button-primary", disabled: true, children: "Sign In" }), jsx("div", { style: { textAlign: 'center', marginTop: '1rem' }, children: jsx("button", { className: "auth-link", disabled: true, children: "Forgot password?" }) }), jsxs("div", { style: { textAlign: 'center', marginTop: '0.5rem', fontSize: '0.875rem', color: 'var(--auth-text-muted, #6B7280)' }, children: ["Don't have an account?", ' ', jsx("button", { className: "auth-link", disabled: true, children: "Sign up" })] })] })), hasOtherProviders && (jsxs(Fragment, { children: [showEmail && (jsx("div", { className: "auth-or-divider", children: jsx("span", { children: "or continue with" }) })), jsx("div", { className: "auth-provider-buttons", children: orderedProviders.map(provider => renderProviderButton(provider)) })] }))] })) }));
|
|
11152
|
-
};
|
|
11153
|
-
|
|
11154
|
-
// Default Smartlinks Google OAuth Client ID (public - safe to expose)
|
|
11155
|
-
const DEFAULT_GOOGLE_CLIENT_ID = '696509063554-jdlbjl8vsjt7cr0vgkjkjf3ffnvi3a70.apps.googleusercontent.com';
|
|
11156
|
-
const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAuthSuccess, onAuthError, enabledProviders = ['email', 'google', 'phone'], initialMode = 'login', redirectUrl, theme = 'light', className, customization, skipConfigFetch = false, }) => {
|
|
11157
|
-
const [mode, setMode] = useState(initialMode);
|
|
12077
|
+
const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', customization = {}, }) => {
|
|
12078
|
+
const auth = useAuth();
|
|
11158
12079
|
const [loading, setLoading] = useState(false);
|
|
12080
|
+
const [profile, setProfile] = useState(null);
|
|
11159
12081
|
const [error, setError] = useState();
|
|
11160
|
-
const [
|
|
11161
|
-
|
|
11162
|
-
const [
|
|
11163
|
-
|
|
11164
|
-
const [
|
|
11165
|
-
|
|
11166
|
-
const [
|
|
11167
|
-
const [
|
|
11168
|
-
|
|
11169
|
-
const [
|
|
11170
|
-
const [
|
|
11171
|
-
const
|
|
11172
|
-
|
|
11173
|
-
|
|
11174
|
-
|
|
12082
|
+
const [success, setSuccess] = useState();
|
|
12083
|
+
// Track which section is being edited
|
|
12084
|
+
const [editingSection, setEditingSection] = useState(null);
|
|
12085
|
+
// Profile form state
|
|
12086
|
+
const [displayName, setDisplayName] = useState('');
|
|
12087
|
+
// Email change state
|
|
12088
|
+
const [newEmail, setNewEmail] = useState('');
|
|
12089
|
+
const [emailPassword, setEmailPassword] = useState('');
|
|
12090
|
+
// Password change state
|
|
12091
|
+
const [currentPassword, setCurrentPassword] = useState('');
|
|
12092
|
+
const [newPassword, setNewPassword] = useState('');
|
|
12093
|
+
const [confirmPassword, setConfirmPassword] = useState('');
|
|
12094
|
+
// Phone change state (reuses existing sendPhoneCode flow)
|
|
12095
|
+
const [newPhone, setNewPhone] = useState('');
|
|
12096
|
+
const [phoneCode, setPhoneCode] = useState('');
|
|
12097
|
+
const [phoneCodeSent, setPhoneCodeSent] = useState(false);
|
|
12098
|
+
// Account deletion state
|
|
12099
|
+
const [deletePassword, setDeletePassword] = useState('');
|
|
12100
|
+
const [deleteConfirmText, setDeleteConfirmText] = useState('');
|
|
12101
|
+
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
|
12102
|
+
const { showProfileSection = true, showEmailSection = true, showPasswordSection = true, showPhoneSection = true, showDeleteAccount = false, } = customization;
|
|
12103
|
+
// Reinitialize Smartlinks SDK when apiEndpoint changes
|
|
11175
12104
|
useEffect(() => {
|
|
11176
12105
|
if (apiEndpoint) {
|
|
11177
|
-
// Get current token before reinitializing
|
|
11178
|
-
const currentToken = auth.getToken();
|
|
11179
12106
|
smartlinks.initializeApi({
|
|
11180
12107
|
baseURL: apiEndpoint,
|
|
11181
|
-
proxyMode: false,
|
|
12108
|
+
proxyMode: false,
|
|
11182
12109
|
ngrokSkipBrowserWarning: true,
|
|
11183
12110
|
});
|
|
11184
|
-
// Restore bearer token after reinitialization using auth.verifyToken
|
|
11185
|
-
if (currentToken) {
|
|
11186
|
-
smartlinks.auth.verifyToken(currentToken).catch(err => {
|
|
11187
|
-
console.warn('Failed to restore bearer token after reinit:', err);
|
|
11188
|
-
});
|
|
11189
|
-
}
|
|
11190
12111
|
}
|
|
11191
|
-
}, [apiEndpoint
|
|
11192
|
-
//
|
|
11193
|
-
const getRedirectUrl = () => {
|
|
11194
|
-
if (redirectUrl)
|
|
11195
|
-
return redirectUrl;
|
|
11196
|
-
// Get the full current URL including hash routes
|
|
11197
|
-
// Remove any existing query parameters to avoid duplication
|
|
11198
|
-
const currentUrl = window.location.href.split('?')[0];
|
|
11199
|
-
return currentUrl;
|
|
11200
|
-
};
|
|
11201
|
-
// Fetch UI configuration
|
|
12112
|
+
}, [apiEndpoint]);
|
|
12113
|
+
// Load user profile on mount
|
|
11202
12114
|
useEffect(() => {
|
|
11203
|
-
|
|
11204
|
-
|
|
12115
|
+
loadProfile();
|
|
12116
|
+
}, [clientId]);
|
|
12117
|
+
const loadProfile = async () => {
|
|
12118
|
+
if (!auth.isAuthenticated) {
|
|
12119
|
+
setError('You must be logged in to manage your account');
|
|
11205
12120
|
return;
|
|
11206
12121
|
}
|
|
11207
|
-
const fetchConfig = async () => {
|
|
11208
|
-
try {
|
|
11209
|
-
// Check localStorage cache first
|
|
11210
|
-
const cacheKey = `auth_ui_config_${clientId || 'default'}`;
|
|
11211
|
-
const cached = localStorage.getItem(cacheKey);
|
|
11212
|
-
if (cached) {
|
|
11213
|
-
const { config: cachedConfig, timestamp } = JSON.parse(cached);
|
|
11214
|
-
const age = Date.now() - timestamp;
|
|
11215
|
-
// Use cache if less than 1 hour old
|
|
11216
|
-
if (age < 3600000) {
|
|
11217
|
-
setConfig({ ...cachedConfig, ...customization });
|
|
11218
|
-
setConfigLoading(false);
|
|
11219
|
-
// Fetch in background to update cache
|
|
11220
|
-
api.fetchConfig().then(freshConfig => {
|
|
11221
|
-
localStorage.setItem(cacheKey, JSON.stringify({
|
|
11222
|
-
config: freshConfig,
|
|
11223
|
-
timestamp: Date.now()
|
|
11224
|
-
}));
|
|
11225
|
-
});
|
|
11226
|
-
return;
|
|
11227
|
-
}
|
|
11228
|
-
}
|
|
11229
|
-
// Fetch from API
|
|
11230
|
-
const fetchedConfig = await api.fetchConfig();
|
|
11231
|
-
// Merge with customization props (props take precedence)
|
|
11232
|
-
const mergedConfig = { ...fetchedConfig, ...customization };
|
|
11233
|
-
setConfig(mergedConfig);
|
|
11234
|
-
// Cache the fetched config
|
|
11235
|
-
localStorage.setItem(cacheKey, JSON.stringify({
|
|
11236
|
-
config: fetchedConfig,
|
|
11237
|
-
timestamp: Date.now()
|
|
11238
|
-
}));
|
|
11239
|
-
}
|
|
11240
|
-
catch (err) {
|
|
11241
|
-
console.error('Failed to fetch config:', err);
|
|
11242
|
-
setConfig(customization || {});
|
|
11243
|
-
}
|
|
11244
|
-
finally {
|
|
11245
|
-
setConfigLoading(false);
|
|
11246
|
-
}
|
|
11247
|
-
};
|
|
11248
|
-
fetchConfig();
|
|
11249
|
-
}, [apiEndpoint, clientId, customization, skipConfigFetch]);
|
|
11250
|
-
// Reset showEmailForm when mode changes away from login/register
|
|
11251
|
-
useEffect(() => {
|
|
11252
|
-
if (mode !== 'login' && mode !== 'register') {
|
|
11253
|
-
setShowEmailForm(false);
|
|
11254
|
-
}
|
|
11255
|
-
}, [mode]);
|
|
11256
|
-
// Handle URL-based auth flows (email verification, password reset)
|
|
11257
|
-
useEffect(() => {
|
|
11258
|
-
// Helper to get URL parameters from either hash or search
|
|
11259
|
-
const getUrlParams = () => {
|
|
11260
|
-
// First check if there are params in the hash (for hash routing)
|
|
11261
|
-
const hash = window.location.hash;
|
|
11262
|
-
const hashQueryIndex = hash.indexOf('?');
|
|
11263
|
-
if (hashQueryIndex !== -1) {
|
|
11264
|
-
// Extract query string from hash (e.g., #/test?mode=verifyEmail&token=abc)
|
|
11265
|
-
const hashQuery = hash.substring(hashQueryIndex + 1);
|
|
11266
|
-
return new URLSearchParams(hashQuery);
|
|
11267
|
-
}
|
|
11268
|
-
// Fall back to regular search params (for non-hash routing)
|
|
11269
|
-
return new URLSearchParams(window.location.search);
|
|
11270
|
-
};
|
|
11271
|
-
const params = getUrlParams();
|
|
11272
|
-
const urlMode = params.get('mode');
|
|
11273
|
-
const token = params.get('token');
|
|
11274
|
-
console.log('URL params detected:', { urlMode, token, hash: window.location.hash, search: window.location.search });
|
|
11275
|
-
if (urlMode && token) {
|
|
11276
|
-
handleURLBasedAuth(urlMode, token);
|
|
11277
|
-
}
|
|
11278
|
-
}, []);
|
|
11279
|
-
const handleURLBasedAuth = async (urlMode, token) => {
|
|
11280
|
-
setLoading(true);
|
|
11281
|
-
setError(undefined);
|
|
11282
|
-
try {
|
|
11283
|
-
if (urlMode === 'verifyEmail') {
|
|
11284
|
-
console.log('Verifying email with token:', token);
|
|
11285
|
-
const response = await api.verifyEmailWithToken(token);
|
|
11286
|
-
// Get email verification mode from response or config
|
|
11287
|
-
const verificationMode = response.emailVerificationMode || config?.emailVerification?.mode || 'verify-then-auto-login';
|
|
11288
|
-
if ((verificationMode === 'verify-then-auto-login' || verificationMode === 'immediate') && response.token) {
|
|
11289
|
-
// Auto-login modes: Log the user in immediately if token is provided
|
|
11290
|
-
auth.login(response.token, response.user, response.accountData);
|
|
11291
|
-
setAuthSuccess(true);
|
|
11292
|
-
setSuccessMessage('Email verified successfully! You are now logged in.');
|
|
11293
|
-
onAuthSuccess(response.token, response.user, response.accountData);
|
|
11294
|
-
// Clear the URL parameters
|
|
11295
|
-
const cleanUrl = window.location.href.split('?')[0];
|
|
11296
|
-
window.history.replaceState({}, document.title, cleanUrl);
|
|
11297
|
-
// Redirect after a brief delay to show success message
|
|
11298
|
-
if (redirectUrl) {
|
|
11299
|
-
setTimeout(() => {
|
|
11300
|
-
window.location.href = redirectUrl;
|
|
11301
|
-
}, 2000);
|
|
11302
|
-
}
|
|
11303
|
-
}
|
|
11304
|
-
else {
|
|
11305
|
-
// verify-then-manual-login mode or no token: Show success but require manual login
|
|
11306
|
-
setAuthSuccess(true);
|
|
11307
|
-
setSuccessMessage('Email verified successfully! Please log in with your credentials.');
|
|
11308
|
-
// Clear the URL parameters
|
|
11309
|
-
const cleanUrl = window.location.href.split('?')[0];
|
|
11310
|
-
window.history.replaceState({}, document.title, cleanUrl);
|
|
11311
|
-
// Switch back to login mode after a delay
|
|
11312
|
-
setTimeout(() => {
|
|
11313
|
-
setAuthSuccess(false);
|
|
11314
|
-
setMode('login');
|
|
11315
|
-
}, 3000);
|
|
11316
|
-
}
|
|
11317
|
-
}
|
|
11318
|
-
else if (urlMode === 'resetPassword') {
|
|
11319
|
-
console.log('Verifying reset token:', token);
|
|
11320
|
-
// Verify token is valid, then show password reset form
|
|
11321
|
-
await api.verifyResetToken(token);
|
|
11322
|
-
setResetToken(token); // Store token for use in password reset
|
|
11323
|
-
setMode('reset-password');
|
|
11324
|
-
// Clear the URL parameters
|
|
11325
|
-
const cleanUrl = window.location.href.split('?')[0];
|
|
11326
|
-
window.history.replaceState({}, document.title, cleanUrl);
|
|
11327
|
-
}
|
|
11328
|
-
else if (urlMode === 'magicLink') {
|
|
11329
|
-
console.log('Verifying magic link token:', token);
|
|
11330
|
-
const response = await api.verifyMagicLink(token);
|
|
11331
|
-
// Auto-login with magic link if token is provided
|
|
11332
|
-
if (response.token) {
|
|
11333
|
-
auth.login(response.token, response.user, response.accountData);
|
|
11334
|
-
setAuthSuccess(true);
|
|
11335
|
-
setSuccessMessage('Magic link verified! You are now logged in.');
|
|
11336
|
-
onAuthSuccess(response.token, response.user, response.accountData);
|
|
11337
|
-
// Clear the URL parameters
|
|
11338
|
-
const cleanUrl = window.location.href.split('?')[0];
|
|
11339
|
-
window.history.replaceState({}, document.title, cleanUrl);
|
|
11340
|
-
// Redirect after a brief delay to show success message
|
|
11341
|
-
if (redirectUrl) {
|
|
11342
|
-
setTimeout(() => {
|
|
11343
|
-
window.location.href = redirectUrl;
|
|
11344
|
-
}, 2000);
|
|
11345
|
-
}
|
|
11346
|
-
}
|
|
11347
|
-
else {
|
|
11348
|
-
throw new Error('Authentication failed - no token received');
|
|
11349
|
-
}
|
|
11350
|
-
}
|
|
11351
|
-
}
|
|
11352
|
-
catch (err) {
|
|
11353
|
-
console.error('URL-based auth error:', err);
|
|
11354
|
-
const errorMessage = err instanceof Error ? err.message : 'An error occurred';
|
|
11355
|
-
// If it's an email verification error (expired/invalid token), show resend option
|
|
11356
|
-
if (urlMode === 'verifyEmail') {
|
|
11357
|
-
setError(`${errorMessage} - Please enter your email below to receive a new verification link.`);
|
|
11358
|
-
setShowResendVerification(true);
|
|
11359
|
-
setMode('login'); // Show the login form UI
|
|
11360
|
-
// Clear the URL parameters
|
|
11361
|
-
const cleanUrl = window.location.href.split('?')[0];
|
|
11362
|
-
window.history.replaceState({}, document.title, cleanUrl);
|
|
11363
|
-
}
|
|
11364
|
-
else if (urlMode === 'resetPassword') {
|
|
11365
|
-
// If password reset token is invalid/expired, show request new reset link option
|
|
11366
|
-
setError(`${errorMessage} - Please enter your email below to receive a new password reset link.`);
|
|
11367
|
-
setShowRequestNewReset(true);
|
|
11368
|
-
setMode('login');
|
|
11369
|
-
// Clear the URL parameters
|
|
11370
|
-
const cleanUrl = window.location.href.split('?')[0];
|
|
11371
|
-
window.history.replaceState({}, document.title, cleanUrl);
|
|
11372
|
-
}
|
|
11373
|
-
else if (urlMode === 'magicLink') {
|
|
11374
|
-
// If magic link is invalid/expired
|
|
11375
|
-
setError(`${errorMessage} - Please request a new magic link below.`);
|
|
11376
|
-
setMode('magic-link');
|
|
11377
|
-
// Clear the URL parameters
|
|
11378
|
-
const cleanUrl = window.location.href.split('?')[0];
|
|
11379
|
-
window.history.replaceState({}, document.title, cleanUrl);
|
|
11380
|
-
}
|
|
11381
|
-
else {
|
|
11382
|
-
setError(errorMessage);
|
|
11383
|
-
}
|
|
11384
|
-
onAuthError?.(err instanceof Error ? err : new Error('An error occurred'));
|
|
11385
|
-
}
|
|
11386
|
-
finally {
|
|
11387
|
-
setLoading(false);
|
|
11388
|
-
}
|
|
11389
|
-
};
|
|
11390
|
-
const handleEmailAuth = async (data) => {
|
|
11391
12122
|
setLoading(true);
|
|
11392
12123
|
setError(undefined);
|
|
11393
|
-
setAuthSuccess(false);
|
|
11394
12124
|
try {
|
|
11395
|
-
|
|
11396
|
-
|
|
11397
|
-
|
|
11398
|
-
|
|
11399
|
-
|
|
11400
|
-
|
|
11401
|
-
|
|
11402
|
-
|
|
11403
|
-
|
|
11404
|
-
|
|
11405
|
-
|
|
11406
|
-
|
|
11407
|
-
|
|
11408
|
-
|
|
11409
|
-
|
|
11410
|
-
setAuthSuccess(true);
|
|
11411
|
-
const deadline = response.emailVerificationDeadline
|
|
11412
|
-
? new Date(response.emailVerificationDeadline).toLocaleString()
|
|
11413
|
-
: `${gracePeriodHours} hours`;
|
|
11414
|
-
setSuccessMessage(`Account created! You're logged in now. Please verify your email by ${deadline} to keep your account active.`);
|
|
11415
|
-
if (response.token) {
|
|
11416
|
-
onAuthSuccess(response.token, response.user, response.accountData);
|
|
11417
|
-
}
|
|
11418
|
-
if (redirectUrl) {
|
|
11419
|
-
setTimeout(() => {
|
|
11420
|
-
window.location.href = redirectUrl;
|
|
11421
|
-
}, 2000);
|
|
11422
|
-
}
|
|
11423
|
-
}
|
|
11424
|
-
else if (verificationMode === 'verify-then-auto-login') {
|
|
11425
|
-
// Verify-then-auto-login mode: Don't log in yet, but will auto-login after email verification
|
|
11426
|
-
setAuthSuccess(true);
|
|
11427
|
-
setSuccessMessage('Account created! Please check your email and click the verification link to complete your registration.');
|
|
11428
|
-
}
|
|
11429
|
-
else {
|
|
11430
|
-
// verify-then-manual-login mode: Traditional flow
|
|
11431
|
-
setAuthSuccess(true);
|
|
11432
|
-
setSuccessMessage('Account created successfully! Please check your email to verify your account, then log in.');
|
|
11433
|
-
}
|
|
11434
|
-
}
|
|
11435
|
-
else {
|
|
11436
|
-
// Login mode - always log in if token is provided
|
|
11437
|
-
if (response.token) {
|
|
11438
|
-
// Check for account lock or verification requirements
|
|
11439
|
-
if (response.accountLocked) {
|
|
11440
|
-
throw new Error('Your account has been locked due to unverified email. Please check your email or request a new verification link.');
|
|
11441
|
-
}
|
|
11442
|
-
if (response.requiresEmailVerification) {
|
|
11443
|
-
throw new Error('Please verify your email before logging in. Check your inbox for the verification link.');
|
|
11444
|
-
}
|
|
11445
|
-
auth.login(response.token, response.user, response.accountData);
|
|
11446
|
-
setAuthSuccess(true);
|
|
11447
|
-
setSuccessMessage('Login successful!');
|
|
11448
|
-
onAuthSuccess(response.token, response.user, response.accountData);
|
|
11449
|
-
if (redirectUrl) {
|
|
11450
|
-
setTimeout(() => {
|
|
11451
|
-
window.location.href = redirectUrl;
|
|
11452
|
-
}, 2000);
|
|
11453
|
-
}
|
|
11454
|
-
}
|
|
11455
|
-
else {
|
|
11456
|
-
throw new Error('Authentication failed - please verify your email before logging in.');
|
|
11457
|
-
}
|
|
11458
|
-
}
|
|
12125
|
+
// TODO: Backend implementation required
|
|
12126
|
+
// Endpoint: GET /api/v1/authkit/:clientId/account/profile
|
|
12127
|
+
// SDK method: smartlinks.authKit.getProfile(clientId)
|
|
12128
|
+
// Temporary mock data for UI testing
|
|
12129
|
+
const profileData = {
|
|
12130
|
+
uid: auth.user?.uid || '',
|
|
12131
|
+
email: auth.user?.email,
|
|
12132
|
+
displayName: auth.user?.displayName,
|
|
12133
|
+
phoneNumber: auth.user?.phoneNumber,
|
|
12134
|
+
photoURL: auth.user?.photoURL,
|
|
12135
|
+
emailVerified: true,
|
|
12136
|
+
accountData: auth.accountData || {},
|
|
12137
|
+
};
|
|
12138
|
+
setProfile(profileData);
|
|
12139
|
+
setDisplayName(profileData.displayName || '');
|
|
11459
12140
|
}
|
|
11460
12141
|
catch (err) {
|
|
11461
|
-
const errorMessage = err instanceof Error ? err.message : '
|
|
11462
|
-
|
|
11463
|
-
|
|
11464
|
-
setShowResendVerification(true);
|
|
11465
|
-
setResendEmail(data.email);
|
|
11466
|
-
setError('This email is already registered. If you didn\'t receive the verification email, you can resend it below.');
|
|
11467
|
-
}
|
|
11468
|
-
else {
|
|
11469
|
-
setError(errorMessage);
|
|
11470
|
-
}
|
|
11471
|
-
onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
12142
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to load profile';
|
|
12143
|
+
setError(errorMessage);
|
|
12144
|
+
onError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
11472
12145
|
}
|
|
11473
12146
|
finally {
|
|
11474
12147
|
setLoading(false);
|
|
11475
12148
|
}
|
|
11476
12149
|
};
|
|
11477
|
-
const
|
|
11478
|
-
|
|
11479
|
-
return;
|
|
12150
|
+
const handleUpdateProfile = async (e) => {
|
|
12151
|
+
e.preventDefault();
|
|
11480
12152
|
setLoading(true);
|
|
11481
12153
|
setError(undefined);
|
|
12154
|
+
setSuccess(undefined);
|
|
11482
12155
|
try {
|
|
11483
|
-
//
|
|
11484
|
-
//
|
|
11485
|
-
|
|
11486
|
-
|
|
11487
|
-
|
|
11488
|
-
|
|
12156
|
+
// TODO: Backend implementation required
|
|
12157
|
+
// Endpoint: POST /api/v1/authkit/:clientId/account/update-profile
|
|
12158
|
+
// SDK method: smartlinks.authKit.updateProfile(clientId, updateData)
|
|
12159
|
+
setError('Backend API not yet implemented. See console for required endpoint.');
|
|
12160
|
+
console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/update-profile');
|
|
12161
|
+
console.log('Update data:', { displayName });
|
|
12162
|
+
// Uncomment when backend is ready:
|
|
12163
|
+
// const updateData: ProfileUpdateData = {
|
|
12164
|
+
// displayName: displayName || undefined,
|
|
12165
|
+
// photoURL: photoURL || undefined,
|
|
12166
|
+
// };
|
|
12167
|
+
// const updatedProfile = await smartlinks.authKit.updateProfile(clientId, updateData);
|
|
12168
|
+
// setProfile(updatedProfile);
|
|
12169
|
+
// setSuccess('Profile updated successfully!');
|
|
12170
|
+
// setEditingSection(null);
|
|
12171
|
+
// onProfileUpdated?.(updatedProfile);
|
|
11489
12172
|
}
|
|
11490
12173
|
catch (err) {
|
|
11491
|
-
const errorMessage = err instanceof Error ? err.message : 'Failed to
|
|
12174
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to update profile';
|
|
11492
12175
|
setError(errorMessage);
|
|
11493
|
-
|
|
12176
|
+
onError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
11494
12177
|
}
|
|
11495
12178
|
finally {
|
|
11496
12179
|
setLoading(false);
|
|
11497
12180
|
}
|
|
11498
12181
|
};
|
|
11499
|
-
const
|
|
11500
|
-
|
|
11501
|
-
|
|
12182
|
+
const cancelEdit = () => {
|
|
12183
|
+
setEditingSection(null);
|
|
12184
|
+
setDisplayName(profile?.displayName || '');
|
|
12185
|
+
setNewEmail('');
|
|
12186
|
+
setEmailPassword('');
|
|
12187
|
+
setCurrentPassword('');
|
|
12188
|
+
setNewPassword('');
|
|
12189
|
+
setConfirmPassword('');
|
|
12190
|
+
setNewPhone('');
|
|
12191
|
+
setPhoneCode('');
|
|
12192
|
+
setPhoneCodeSent(false);
|
|
12193
|
+
setError(undefined);
|
|
12194
|
+
setSuccess(undefined);
|
|
12195
|
+
};
|
|
12196
|
+
const handleChangeEmail = async (e) => {
|
|
12197
|
+
e.preventDefault();
|
|
11502
12198
|
setLoading(true);
|
|
11503
12199
|
setError(undefined);
|
|
12200
|
+
setSuccess(undefined);
|
|
11504
12201
|
try {
|
|
11505
|
-
|
|
11506
|
-
|
|
11507
|
-
|
|
11508
|
-
|
|
11509
|
-
|
|
12202
|
+
// TODO: Backend implementation required
|
|
12203
|
+
// Endpoint: POST /api/v1/authkit/:clientId/account/change-email
|
|
12204
|
+
// SDK method: smartlinks.authKit.changeEmail(clientId, newEmail, password)
|
|
12205
|
+
// Note: No verification flow for now - direct email update
|
|
12206
|
+
setError('Backend API not yet implemented. See console for required endpoint.');
|
|
12207
|
+
console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/change-email');
|
|
12208
|
+
console.log('Data:', { newEmail });
|
|
12209
|
+
// Uncomment when backend is ready:
|
|
12210
|
+
// await smartlinks.authKit.changeEmail(clientId, newEmail, emailPassword);
|
|
12211
|
+
// setSuccess('Email changed successfully!');
|
|
12212
|
+
// setEditingSection(null);
|
|
12213
|
+
// setNewEmail('');
|
|
12214
|
+
// setEmailPassword('');
|
|
12215
|
+
// onEmailChangeRequested?.();
|
|
12216
|
+
// await loadProfile(); // Reload to show new email
|
|
11510
12217
|
}
|
|
11511
12218
|
catch (err) {
|
|
11512
|
-
const errorMessage = err instanceof Error ? err.message : 'Failed to
|
|
12219
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to change email';
|
|
11513
12220
|
setError(errorMessage);
|
|
11514
|
-
|
|
12221
|
+
onError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
11515
12222
|
}
|
|
11516
12223
|
finally {
|
|
11517
12224
|
setLoading(false);
|
|
11518
12225
|
}
|
|
11519
12226
|
};
|
|
11520
|
-
const
|
|
11521
|
-
|
|
11522
|
-
|
|
11523
|
-
|
|
11524
|
-
|
|
12227
|
+
const handleChangePassword = async (e) => {
|
|
12228
|
+
e.preventDefault();
|
|
12229
|
+
if (newPassword !== confirmPassword) {
|
|
12230
|
+
setError('New passwords do not match');
|
|
12231
|
+
return;
|
|
12232
|
+
}
|
|
12233
|
+
if (newPassword.length < 6) {
|
|
12234
|
+
setError('Password must be at least 6 characters');
|
|
12235
|
+
return;
|
|
12236
|
+
}
|
|
11525
12237
|
setLoading(true);
|
|
11526
12238
|
setError(undefined);
|
|
12239
|
+
setSuccess(undefined);
|
|
11527
12240
|
try {
|
|
11528
|
-
|
|
11529
|
-
|
|
11530
|
-
|
|
11531
|
-
|
|
11532
|
-
|
|
11533
|
-
|
|
11534
|
-
|
|
11535
|
-
|
|
11536
|
-
|
|
11537
|
-
|
|
11538
|
-
|
|
11539
|
-
|
|
11540
|
-
|
|
11541
|
-
|
|
11542
|
-
if (response.error) {
|
|
11543
|
-
throw new Error(response.error_description || response.error);
|
|
11544
|
-
}
|
|
11545
|
-
const accessToken = response.access_token;
|
|
11546
|
-
// Send access token to backend
|
|
11547
|
-
const authResponse = await api.loginWithGoogle(accessToken);
|
|
11548
|
-
if (authResponse.token) {
|
|
11549
|
-
auth.login(authResponse.token, authResponse.user, authResponse.accountData);
|
|
11550
|
-
setAuthSuccess(true);
|
|
11551
|
-
setSuccessMessage('Google login successful!');
|
|
11552
|
-
onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
|
|
11553
|
-
}
|
|
11554
|
-
else {
|
|
11555
|
-
throw new Error('Authentication failed - no token received');
|
|
11556
|
-
}
|
|
11557
|
-
if (redirectUrl) {
|
|
11558
|
-
setTimeout(() => {
|
|
11559
|
-
window.location.href = redirectUrl;
|
|
11560
|
-
}, 2000);
|
|
11561
|
-
}
|
|
11562
|
-
setLoading(false);
|
|
11563
|
-
}
|
|
11564
|
-
catch (err) {
|
|
11565
|
-
const errorMessage = err instanceof Error ? err.message : 'Google login failed';
|
|
11566
|
-
setError(errorMessage);
|
|
11567
|
-
onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
11568
|
-
setLoading(false);
|
|
11569
|
-
}
|
|
11570
|
-
},
|
|
11571
|
-
});
|
|
11572
|
-
client.requestAccessToken();
|
|
11573
|
-
}
|
|
11574
|
-
else {
|
|
11575
|
-
// Use One Tap / Sign-In button flow (smoother UX but doesn't work in iframes)
|
|
11576
|
-
google.accounts.id.initialize({
|
|
11577
|
-
client_id: googleClientId,
|
|
11578
|
-
callback: async (response) => {
|
|
11579
|
-
try {
|
|
11580
|
-
const idToken = response.credential;
|
|
11581
|
-
const authResponse = await api.loginWithGoogle(idToken);
|
|
11582
|
-
if (authResponse.token) {
|
|
11583
|
-
auth.login(authResponse.token, authResponse.user, authResponse.accountData);
|
|
11584
|
-
setAuthSuccess(true);
|
|
11585
|
-
setSuccessMessage('Google login successful!');
|
|
11586
|
-
onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
|
|
11587
|
-
}
|
|
11588
|
-
else {
|
|
11589
|
-
throw new Error('Authentication failed - no token received');
|
|
11590
|
-
}
|
|
11591
|
-
if (redirectUrl) {
|
|
11592
|
-
setTimeout(() => {
|
|
11593
|
-
window.location.href = redirectUrl;
|
|
11594
|
-
}, 2000);
|
|
11595
|
-
}
|
|
11596
|
-
setLoading(false);
|
|
11597
|
-
}
|
|
11598
|
-
catch (err) {
|
|
11599
|
-
const errorMessage = err instanceof Error ? err.message : 'Google login failed';
|
|
11600
|
-
setError(errorMessage);
|
|
11601
|
-
onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
11602
|
-
setLoading(false);
|
|
11603
|
-
}
|
|
11604
|
-
},
|
|
11605
|
-
auto_select: false,
|
|
11606
|
-
cancel_on_tap_outside: true,
|
|
11607
|
-
});
|
|
11608
|
-
google.accounts.id.prompt((notification) => {
|
|
11609
|
-
if (notification.isNotDisplayed() || notification.isSkippedMoment()) {
|
|
11610
|
-
setLoading(false);
|
|
11611
|
-
}
|
|
11612
|
-
});
|
|
11613
|
-
}
|
|
12241
|
+
// TODO: Backend implementation required
|
|
12242
|
+
// Endpoint: POST /api/v1/authkit/:clientId/account/change-password
|
|
12243
|
+
// SDK method: smartlinks.authKit.changePassword(clientId, currentPassword, newPassword)
|
|
12244
|
+
setError('Backend API not yet implemented. See console for required endpoint.');
|
|
12245
|
+
console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/change-password');
|
|
12246
|
+
console.log('Data: currentPassword and newPassword provided');
|
|
12247
|
+
// Uncomment when backend is ready:
|
|
12248
|
+
// await smartlinks.authKit.changePassword(clientId, currentPassword, newPassword);
|
|
12249
|
+
// setSuccess('Password changed successfully!');
|
|
12250
|
+
// setEditingSection(null);
|
|
12251
|
+
// setCurrentPassword('');
|
|
12252
|
+
// setNewPassword('');
|
|
12253
|
+
// setConfirmPassword('');
|
|
12254
|
+
// onPasswordChanged?.();
|
|
11614
12255
|
}
|
|
11615
12256
|
catch (err) {
|
|
11616
|
-
const errorMessage = err instanceof Error ? err.message : '
|
|
12257
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to change password';
|
|
11617
12258
|
setError(errorMessage);
|
|
11618
|
-
|
|
12259
|
+
onError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
12260
|
+
}
|
|
12261
|
+
finally {
|
|
11619
12262
|
setLoading(false);
|
|
11620
12263
|
}
|
|
11621
12264
|
};
|
|
11622
|
-
const
|
|
12265
|
+
const handleSendPhoneCode = async () => {
|
|
11623
12266
|
setLoading(true);
|
|
11624
12267
|
setError(undefined);
|
|
11625
12268
|
try {
|
|
11626
|
-
|
|
11627
|
-
|
|
11628
|
-
|
|
11629
|
-
|
|
11630
|
-
|
|
11631
|
-
|
|
11632
|
-
|
|
11633
|
-
|
|
11634
|
-
|
|
11635
|
-
|
|
11636
|
-
|
|
11637
|
-
|
|
11638
|
-
|
|
11639
|
-
|
|
11640
|
-
|
|
11641
|
-
|
|
11642
|
-
|
|
11643
|
-
|
|
11644
|
-
|
|
11645
|
-
|
|
11646
|
-
|
|
12269
|
+
await smartlinks.authKit.sendPhoneCode(clientId, newPhone);
|
|
12270
|
+
setPhoneCodeSent(true);
|
|
12271
|
+
setSuccess('Verification code sent to your phone');
|
|
12272
|
+
}
|
|
12273
|
+
catch (err) {
|
|
12274
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to send verification code';
|
|
12275
|
+
setError(errorMessage);
|
|
12276
|
+
onError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
12277
|
+
}
|
|
12278
|
+
finally {
|
|
12279
|
+
setLoading(false);
|
|
12280
|
+
}
|
|
12281
|
+
};
|
|
12282
|
+
const handleUpdatePhone = async (e) => {
|
|
12283
|
+
e.preventDefault();
|
|
12284
|
+
setLoading(true);
|
|
12285
|
+
setError(undefined);
|
|
12286
|
+
setSuccess(undefined);
|
|
12287
|
+
try {
|
|
12288
|
+
// TODO: Backend implementation required
|
|
12289
|
+
// Endpoint: POST /api/v1/authkit/:clientId/account/update-phone
|
|
12290
|
+
// SDK method: smartlinks.authKit.updatePhone(clientId, phoneNumber, verificationCode)
|
|
12291
|
+
setError('Backend API not yet implemented. See console for required endpoint.');
|
|
12292
|
+
console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/update-phone');
|
|
12293
|
+
console.log('Data:', { phoneNumber: newPhone, code: phoneCode });
|
|
12294
|
+
// Uncomment when backend is ready:
|
|
12295
|
+
// await smartlinks.authKit.updatePhone(clientId, newPhone, phoneCode);
|
|
12296
|
+
// setSuccess('Phone number updated successfully!');
|
|
12297
|
+
// setEditingSection(null);
|
|
12298
|
+
// setNewPhone('');
|
|
12299
|
+
// setPhoneCode('');
|
|
12300
|
+
// setPhoneCodeSent(false);
|
|
12301
|
+
// await loadProfile();
|
|
11647
12302
|
}
|
|
11648
12303
|
catch (err) {
|
|
11649
|
-
const errorMessage = err instanceof Error ? err.message : '
|
|
12304
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to update phone number';
|
|
11650
12305
|
setError(errorMessage);
|
|
11651
|
-
|
|
12306
|
+
onError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
11652
12307
|
}
|
|
11653
12308
|
finally {
|
|
11654
12309
|
setLoading(false);
|
|
11655
12310
|
}
|
|
11656
12311
|
};
|
|
11657
|
-
const
|
|
12312
|
+
const handleDeleteAccount = async () => {
|
|
12313
|
+
if (deleteConfirmText !== 'DELETE') {
|
|
12314
|
+
setError('Please type DELETE to confirm account deletion');
|
|
12315
|
+
return;
|
|
12316
|
+
}
|
|
12317
|
+
if (!deletePassword) {
|
|
12318
|
+
setError('Password is required');
|
|
12319
|
+
return;
|
|
12320
|
+
}
|
|
11658
12321
|
setLoading(true);
|
|
11659
12322
|
setError(undefined);
|
|
11660
12323
|
try {
|
|
11661
|
-
|
|
11662
|
-
|
|
11663
|
-
|
|
11664
|
-
|
|
11665
|
-
|
|
11666
|
-
|
|
11667
|
-
|
|
11668
|
-
|
|
11669
|
-
|
|
11670
|
-
|
|
11671
|
-
|
|
12324
|
+
// TODO: Backend implementation required
|
|
12325
|
+
// Endpoint: DELETE /api/v1/authkit/:clientId/account/delete
|
|
12326
|
+
// SDK method: smartlinks.authKit.deleteAccount(clientId, password, confirmText)
|
|
12327
|
+
// Note: This performs a SOFT DELETE (marks as deleted, obfuscates email)
|
|
12328
|
+
setError('Backend API not yet implemented. See console for required endpoint.');
|
|
12329
|
+
console.log('Required API endpoint: DELETE /api/v1/authkit/:clientId/account/delete');
|
|
12330
|
+
console.log('Data: password and confirmText="DELETE" provided');
|
|
12331
|
+
console.log('Note: Backend should soft delete (mark deleted, obfuscate email, disable account)');
|
|
12332
|
+
// Uncomment when backend is ready:
|
|
12333
|
+
// await smartlinks.authKit.deleteAccount(clientId, deletePassword, deleteConfirmText);
|
|
12334
|
+
// setSuccess('Account deleted successfully');
|
|
12335
|
+
// onAccountDeleted?.();
|
|
12336
|
+
// await auth.logout();
|
|
11672
12337
|
}
|
|
11673
12338
|
catch (err) {
|
|
11674
|
-
const errorMessage = err instanceof Error ? err.message : '
|
|
12339
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to delete account';
|
|
11675
12340
|
setError(errorMessage);
|
|
11676
|
-
|
|
12341
|
+
onError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
11677
12342
|
}
|
|
11678
12343
|
finally {
|
|
11679
12344
|
setLoading(false);
|
|
11680
12345
|
}
|
|
11681
12346
|
};
|
|
11682
|
-
|
|
12347
|
+
if (!auth.isAuthenticated) {
|
|
12348
|
+
return (jsx("div", { className: `account-management ${className}`, children: jsx("p", { className: "text-muted-foreground", children: "Please log in to manage your account" }) }));
|
|
12349
|
+
}
|
|
12350
|
+
if (loading && !profile) {
|
|
12351
|
+
return (jsx("div", { className: `account-management ${className}`, children: jsx("p", { className: "text-muted-foreground", children: "Loading..." }) }));
|
|
12352
|
+
}
|
|
12353
|
+
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" })] })] }))] })), 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: () => {
|
|
12354
|
+
setShowDeleteConfirm(false);
|
|
12355
|
+
setDeletePassword('');
|
|
12356
|
+
setDeleteConfirmText('');
|
|
12357
|
+
}, className: "auth-button button-secondary", children: "Cancel" })] })] }))] }))] }));
|
|
12358
|
+
};
|
|
12359
|
+
|
|
12360
|
+
const SmartlinksClaimUI = ({ apiEndpoint, clientId, clientName, collectionId, productId, proofId, onClaimSuccess, onClaimError, additionalFields = [], theme = 'light', className = '', minimal = false, customization = {}, }) => {
|
|
12361
|
+
const auth = useAuth();
|
|
12362
|
+
const [claimStep, setClaimStep] = useState(auth.isAuthenticated ? 'questions' : 'auth');
|
|
12363
|
+
const [claimData, setClaimData] = useState({});
|
|
12364
|
+
const [error, setError] = useState();
|
|
12365
|
+
const [loading, setLoading] = useState(false);
|
|
12366
|
+
const handleAuthSuccess = (token, user, accountData) => {
|
|
12367
|
+
// Authentication successful
|
|
12368
|
+
auth.login(token, user, accountData);
|
|
12369
|
+
// If no additional questions, proceed directly to claim
|
|
12370
|
+
if (additionalFields.length === 0) {
|
|
12371
|
+
executeClaim(user);
|
|
12372
|
+
}
|
|
12373
|
+
else {
|
|
12374
|
+
setClaimStep('questions');
|
|
12375
|
+
}
|
|
12376
|
+
};
|
|
12377
|
+
const handleQuestionSubmit = async (e) => {
|
|
12378
|
+
e.preventDefault();
|
|
12379
|
+
// Validate required fields
|
|
12380
|
+
const missingFields = additionalFields
|
|
12381
|
+
.filter(field => field.required && !claimData[field.name])
|
|
12382
|
+
.map(field => field.label);
|
|
12383
|
+
if (missingFields.length > 0) {
|
|
12384
|
+
setError(`Please fill in: ${missingFields.join(', ')}`);
|
|
12385
|
+
return;
|
|
12386
|
+
}
|
|
12387
|
+
// Execute claim with collected data
|
|
12388
|
+
if (auth.user) {
|
|
12389
|
+
executeClaim(auth.user);
|
|
12390
|
+
}
|
|
12391
|
+
};
|
|
12392
|
+
const executeClaim = async (user) => {
|
|
12393
|
+
setClaimStep('claiming');
|
|
11683
12394
|
setLoading(true);
|
|
11684
12395
|
setError(undefined);
|
|
11685
12396
|
try {
|
|
11686
|
-
|
|
11687
|
-
|
|
11688
|
-
|
|
12397
|
+
// Create attestation to claim the proof
|
|
12398
|
+
const response = await smartlinks.attestation.create(collectionId, productId, proofId, {
|
|
12399
|
+
public: {
|
|
12400
|
+
claimed: true,
|
|
12401
|
+
claimedAt: new Date().toISOString(),
|
|
12402
|
+
claimedBy: user.uid,
|
|
12403
|
+
...claimData,
|
|
12404
|
+
},
|
|
12405
|
+
private: {},
|
|
12406
|
+
proof: {},
|
|
12407
|
+
});
|
|
12408
|
+
setClaimStep('success');
|
|
12409
|
+
// Call success callback
|
|
12410
|
+
onClaimSuccess({
|
|
12411
|
+
proofId,
|
|
12412
|
+
user,
|
|
12413
|
+
claimData,
|
|
12414
|
+
attestationId: response.id,
|
|
12415
|
+
});
|
|
11689
12416
|
}
|
|
11690
12417
|
catch (err) {
|
|
11691
|
-
|
|
12418
|
+
console.error('Claim error:', err);
|
|
12419
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to claim proof';
|
|
11692
12420
|
setError(errorMessage);
|
|
11693
|
-
|
|
12421
|
+
onClaimError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
12422
|
+
setClaimStep(additionalFields.length > 0 ? 'questions' : 'auth');
|
|
11694
12423
|
}
|
|
11695
12424
|
finally {
|
|
11696
12425
|
setLoading(false);
|
|
11697
12426
|
}
|
|
11698
12427
|
};
|
|
11699
|
-
|
|
11700
|
-
|
|
12428
|
+
const handleFieldChange = (fieldName, value) => {
|
|
12429
|
+
setClaimData(prev => ({
|
|
12430
|
+
...prev,
|
|
12431
|
+
[fieldName]: value,
|
|
12432
|
+
}));
|
|
12433
|
+
};
|
|
12434
|
+
// Render authentication step
|
|
12435
|
+
if (claimStep === 'auth') {
|
|
12436
|
+
return (jsx("div", { className: className, children: jsx(SmartlinksAuthUI, { apiEndpoint: apiEndpoint, clientId: clientId, clientName: clientName, onAuthSuccess: handleAuthSuccess, onAuthError: onClaimError, theme: theme, minimal: minimal, customization: customization.authConfig }) }));
|
|
11701
12437
|
}
|
|
11702
|
-
|
|
11703
|
-
|
|
11704
|
-
|
|
11705
|
-
|
|
11706
|
-
|
|
11707
|
-
|
|
11708
|
-
|
|
11709
|
-
|
|
11710
|
-
|
|
11711
|
-
|
|
11712
|
-
|
|
11713
|
-
|
|
11714
|
-
|
|
11715
|
-
|
|
11716
|
-
|
|
11717
|
-
|
|
11718
|
-
|
|
11719
|
-
|
|
11720
|
-
|
|
11721
|
-
|
|
11722
|
-
|
|
11723
|
-
|
|
11724
|
-
|
|
11725
|
-
|
|
11726
|
-
|
|
11727
|
-
|
|
11728
|
-
|
|
11729
|
-
|
|
11730
|
-
|
|
11731
|
-
|
|
11732
|
-
|
|
11733
|
-
|
|
11734
|
-
|
|
11735
|
-
|
|
11736
|
-
|
|
11737
|
-
|
|
11738
|
-
|
|
11739
|
-
|
|
11740
|
-
|
|
11741
|
-
|
|
11742
|
-
|
|
11743
|
-
|
|
11744
|
-
|
|
11745
|
-
|
|
11746
|
-
|
|
11747
|
-
|
|
11748
|
-
|
|
11749
|
-
|
|
11750
|
-
|
|
11751
|
-
|
|
11752
|
-
|
|
11753
|
-
|
|
11754
|
-
|
|
11755
|
-
|
|
11756
|
-
|
|
11757
|
-
|
|
11758
|
-
|
|
11759
|
-
|
|
11760
|
-
|
|
11761
|
-
|
|
11762
|
-
|
|
11763
|
-
|
|
11764
|
-
color: 'white',
|
|
11765
|
-
border: 'none',
|
|
11766
|
-
borderRadius: '0.375rem',
|
|
11767
|
-
cursor: (loading || !resetRequestEmail) ? 'not-allowed' : 'pointer',
|
|
11768
|
-
fontSize: '0.875rem',
|
|
11769
|
-
fontWeight: 500,
|
|
11770
|
-
opacity: (loading || !resetRequestEmail) ? 0.6 : 1
|
|
11771
|
-
}, children: loading ? 'Sending...' : 'Send New Reset Link' }), jsx("button", { onClick: () => {
|
|
11772
|
-
setShowRequestNewReset(false);
|
|
11773
|
-
setResetRequestEmail('');
|
|
11774
|
-
setError(undefined);
|
|
11775
|
-
}, disabled: loading, style: {
|
|
11776
|
-
padding: '0.625rem 1rem',
|
|
11777
|
-
backgroundColor: 'transparent',
|
|
11778
|
-
color: 'var(--auth-text-color, #6B7280)',
|
|
11779
|
-
border: '1px solid var(--auth-border-color, #D1D5DB)',
|
|
11780
|
-
borderRadius: '0.375rem',
|
|
11781
|
-
cursor: loading ? 'not-allowed' : 'pointer',
|
|
11782
|
-
fontSize: '0.875rem',
|
|
11783
|
-
fontWeight: 500,
|
|
11784
|
-
opacity: loading ? 0.6 : 1
|
|
11785
|
-
}, children: "Cancel" })] })] })) : (jsx(Fragment, { children: (() => {
|
|
11786
|
-
const emailDisplayMode = config?.emailDisplayMode || 'form';
|
|
11787
|
-
const providerOrder = config?.providerOrder || (config?.enabledProviders || enabledProviders);
|
|
11788
|
-
const actualProviders = config?.enabledProviders || enabledProviders;
|
|
11789
|
-
// Button mode: show provider selection first, then email form if email is selected
|
|
11790
|
-
if (emailDisplayMode === 'button' && !showEmailForm) {
|
|
11791
|
-
return (jsx(ProviderButtons, { enabledProviders: actualProviders, providerOrder: providerOrder, onEmailLogin: () => setShowEmailForm(true), onGoogleLogin: handleGoogleLogin, onPhoneLogin: () => setMode('phone'), onMagicLinkLogin: () => setMode('magic-link'), loading: loading }));
|
|
11792
|
-
}
|
|
11793
|
-
// Form mode or email button was clicked: show email form with other providers
|
|
11794
|
-
return (jsxs(Fragment, { children: [emailDisplayMode === 'button' && showEmailForm && (jsx("button", { onClick: () => setShowEmailForm(false), style: {
|
|
11795
|
-
marginBottom: '1rem',
|
|
11796
|
-
padding: '0.5rem',
|
|
11797
|
-
background: 'none',
|
|
11798
|
-
border: 'none',
|
|
11799
|
-
color: 'var(--auth-text-color, #6B7280)',
|
|
11800
|
-
cursor: 'pointer',
|
|
11801
|
-
fontSize: '0.875rem',
|
|
11802
|
-
display: 'flex',
|
|
11803
|
-
alignItems: 'center',
|
|
11804
|
-
gap: '0.25rem'
|
|
11805
|
-
}, children: "\u2190 Back to options" })), jsx(EmailAuthForm, { mode: mode, onSubmit: handleEmailAuth, onModeSwitch: () => {
|
|
11806
|
-
setMode(mode === 'login' ? 'register' : 'login');
|
|
11807
|
-
setShowResendVerification(false);
|
|
11808
|
-
setShowRequestNewReset(false);
|
|
11809
|
-
setError(undefined);
|
|
11810
|
-
}, onForgotPassword: () => setMode('reset-password'), loading: loading, error: error }), emailDisplayMode === 'form' && actualProviders.length > 1 && (jsx(ProviderButtons, { enabledProviders: actualProviders.filter((p) => p !== 'email'), providerOrder: providerOrder, onGoogleLogin: handleGoogleLogin, onPhoneLogin: () => setMode('phone'), onMagicLinkLogin: () => setMode('magic-link'), loading: loading }))] }));
|
|
11811
|
-
})() })) })) : null }));
|
|
12438
|
+
// Render additional questions step
|
|
12439
|
+
if (claimStep === 'questions') {
|
|
12440
|
+
return (jsxs("div", { className: `claim-questions ${className}`, children: [jsxs("div", { className: "claim-header mb-6", children: [jsx("h2", { className: "text-2xl font-bold mb-2", children: customization.claimTitle || 'Complete Your Claim' }), customization.claimDescription && (jsx("p", { className: "text-muted-foreground", children: customization.claimDescription }))] }), error && (jsx("div", { className: "claim-error bg-destructive/10 text-destructive px-4 py-3 rounded-md mb-4", children: error })), jsxs("form", { onSubmit: handleQuestionSubmit, className: "claim-form space-y-4", children: [additionalFields.map((field) => (jsxs("div", { className: "claim-field", children: [jsxs("label", { htmlFor: field.name, className: "block text-sm font-medium mb-2", children: [field.label, field.required && jsx("span", { className: "text-destructive ml-1", children: "*" })] }), field.type === 'textarea' ? (jsx("textarea", { id: field.name, name: field.name, placeholder: field.placeholder, value: claimData[field.name] || '', onChange: (e) => handleFieldChange(field.name, e.target.value), required: field.required, className: "w-full px-3 py-2 border border-input rounded-md bg-background", rows: 4 })) : field.type === 'select' ? (jsxs("select", { id: field.name, name: field.name, value: claimData[field.name] || '', onChange: (e) => handleFieldChange(field.name, e.target.value), required: field.required, className: "w-full px-3 py-2 border border-input rounded-md bg-background", children: [jsx("option", { value: "", children: "Select..." }), field.options?.map((option) => (jsx("option", { value: option, children: option }, option)))] })) : (jsx("input", { id: field.name, name: field.name, type: field.type, placeholder: field.placeholder, value: claimData[field.name] || '', onChange: (e) => handleFieldChange(field.name, e.target.value), required: field.required, className: "w-full px-3 py-2 border border-input rounded-md bg-background" }))] }, field.name))), jsx("button", { type: "submit", disabled: loading, className: "claim-submit-button w-full bg-primary text-primary-foreground px-4 py-2 rounded-md font-medium hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed", children: loading ? 'Claiming...' : 'Submit Claim' })] })] }));
|
|
12441
|
+
}
|
|
12442
|
+
// Render claiming step (loading state)
|
|
12443
|
+
if (claimStep === 'claiming') {
|
|
12444
|
+
return (jsxs("div", { className: `claim-loading ${className} flex flex-col items-center justify-center py-12`, children: [jsx("div", { className: "claim-spinner w-12 h-12 border-4 border-primary border-t-transparent rounded-full animate-spin mb-4" }), jsx("p", { className: "text-muted-foreground", children: "Claiming your product..." })] }));
|
|
12445
|
+
}
|
|
12446
|
+
// Render success step
|
|
12447
|
+
if (claimStep === 'success') {
|
|
12448
|
+
return (jsxs("div", { className: `claim-success ${className} text-center py-12`, children: [jsx("div", { className: "claim-success-icon w-16 h-16 bg-green-500 text-white rounded-full flex items-center justify-center text-3xl font-bold mx-auto mb-4", children: "\u2713" }), jsx("h2", { className: "text-2xl font-bold mb-2", children: "Claim Successful!" }), jsx("p", { className: "text-muted-foreground", children: customization.successMessage || 'Your product has been successfully claimed and registered to your account.' })] }));
|
|
12449
|
+
}
|
|
12450
|
+
return null;
|
|
12451
|
+
};
|
|
12452
|
+
|
|
12453
|
+
const ProtectedRoute = ({ children, fallback, redirectTo, }) => {
|
|
12454
|
+
const { isAuthenticated, isLoading } = useAuth();
|
|
12455
|
+
// Show loading state
|
|
12456
|
+
if (isLoading) {
|
|
12457
|
+
return jsx("div", { children: "Loading..." });
|
|
12458
|
+
}
|
|
12459
|
+
// If not authenticated, redirect or show fallback
|
|
12460
|
+
if (!isAuthenticated) {
|
|
12461
|
+
if (redirectTo) {
|
|
12462
|
+
window.location.href = redirectTo;
|
|
12463
|
+
return null;
|
|
12464
|
+
}
|
|
12465
|
+
return fallback ? jsx(Fragment, { children: fallback }) : jsx("div", { children: "Access denied. Please log in." });
|
|
12466
|
+
}
|
|
12467
|
+
// Render protected content
|
|
12468
|
+
return jsx(Fragment, { children: children });
|
|
12469
|
+
};
|
|
12470
|
+
|
|
12471
|
+
const AuthUIPreview = ({ customization, enabledProviders = ['email', 'google', 'phone'], providerOrder, emailDisplayMode = 'form', theme = 'light', className, minimal = false, }) => {
|
|
12472
|
+
const showEmail = enabledProviders.includes('email');
|
|
12473
|
+
const showGoogle = enabledProviders.includes('google');
|
|
12474
|
+
const showPhone = enabledProviders.includes('phone');
|
|
12475
|
+
const showMagicLink = enabledProviders.includes('magic-link');
|
|
12476
|
+
// Determine ordered providers (excluding email if in button mode)
|
|
12477
|
+
const orderedProviders = providerOrder && providerOrder.length > 0
|
|
12478
|
+
? providerOrder.filter(p => enabledProviders.includes(p) && p !== 'email')
|
|
12479
|
+
: enabledProviders.filter(p => p !== 'email');
|
|
12480
|
+
const hasOtherProviders = showGoogle || showPhone || showMagicLink;
|
|
12481
|
+
// Render provider button helper
|
|
12482
|
+
const renderProviderButton = (provider) => {
|
|
12483
|
+
if (provider === 'google' && showGoogle) {
|
|
12484
|
+
return (jsxs("button", { className: "auth-provider-button", disabled: true, children: [jsxs("svg", { width: "18", height: "18", viewBox: "0 0 18 18", fill: "none", children: [jsx("path", { d: "M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844c-.209 1.125-.843 2.078-1.796 2.717v2.258h2.908c1.702-1.567 2.684-3.874 2.684-6.615z", fill: "#4285F4" }), jsx("path", { d: "M9 18c2.43 0 4.467-.806 5.956-2.183l-2.908-2.259c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332C2.438 15.983 5.482 18 9 18z", fill: "#34A853" }), jsx("path", { d: "M3.964 10.707c-.18-.54-.282-1.117-.282-1.707 0-.593.102-1.167.282-1.707V4.961H.957C.347 6.175 0 7.548 0 9s.348 2.825.957 4.039l3.007-2.332z", fill: "#FBBC05" }), jsx("path", { d: "M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0 5.482 0 2.438 2.017.957 4.958L3.964 7.29C4.672 5.163 6.656 3.58 9 3.58z", fill: "#EA4335" })] }), jsx("span", { children: "Continue with Google" })] }, "google"));
|
|
12485
|
+
}
|
|
12486
|
+
if (provider === 'phone' && showPhone) {
|
|
12487
|
+
return (jsxs("button", { className: "auth-provider-button", disabled: true, children: [jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: jsx("path", { d: "M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z" }) }), jsx("span", { children: "Continue with Phone" })] }, "phone"));
|
|
12488
|
+
}
|
|
12489
|
+
if (provider === 'magic-link' && showMagicLink) {
|
|
12490
|
+
return (jsxs("button", { className: "auth-provider-button", disabled: true, children: [jsx("svg", { width: "18", height: "18", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" }) }), jsx("span", { children: "Continue with Magic Link" })] }, "magic-link"));
|
|
12491
|
+
}
|
|
12492
|
+
if (provider === 'email' && showEmail && emailDisplayMode === 'button') {
|
|
12493
|
+
return (jsxs("button", { className: "auth-provider-button", disabled: true, children: [jsx("svg", { width: "18", height: "18", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" }) }), jsx("span", { children: "Continue with Email" })] }, "email"));
|
|
12494
|
+
}
|
|
12495
|
+
return null;
|
|
12496
|
+
};
|
|
12497
|
+
return (jsx(AuthContainer, { theme: theme, className: className, config: customization, minimal: minimal, children: emailDisplayMode === 'button' ? (jsx("div", { className: "auth-provider-buttons", children: orderedProviders.concat(showEmail ? ['email'] : []).map(provider => renderProviderButton(provider)) })) : (
|
|
12498
|
+
/* Form mode: show email form first, then other providers */
|
|
12499
|
+
jsxs(Fragment, { children: [showEmail && (jsxs("div", { className: "auth-form", children: [jsxs("div", { className: "auth-form-group", children: [jsx("label", { className: "auth-label", children: "Email" }), jsx("input", { type: "email", className: "auth-input", placeholder: "Enter your email", disabled: true })] }), jsxs("div", { className: "auth-form-group", children: [jsx("label", { className: "auth-label", children: "Password" }), jsx("input", { type: "password", className: "auth-input", placeholder: "Enter your password", disabled: true })] }), jsx("button", { className: "auth-button auth-button-primary", disabled: true, children: "Sign In" }), jsx("div", { style: { textAlign: 'center', marginTop: '1rem' }, children: jsx("button", { className: "auth-link", disabled: true, children: "Forgot password?" }) }), jsxs("div", { style: { textAlign: 'center', marginTop: '0.5rem', fontSize: '0.875rem', color: 'var(--auth-text-muted, #6B7280)' }, children: ["Don't have an account?", ' ', jsx("button", { className: "auth-link", disabled: true, children: "Sign up" })] })] })), hasOtherProviders && (jsxs(Fragment, { children: [showEmail && (jsx("div", { className: "auth-or-divider", children: jsx("span", { children: "or continue with" }) })), jsx("div", { className: "auth-provider-buttons", children: orderedProviders.map(provider => renderProviderButton(provider)) })] }))] })) }));
|
|
11812
12500
|
};
|
|
11813
12501
|
|
|
11814
|
-
export { AccountManagement, AuthProvider, AuthUIPreview, SmartlinksAuthUI as FirebaseAuthUI, ProtectedRoute, SmartlinksAuthUI, tokenStorage, useAuth };
|
|
12502
|
+
export { AccountManagement, AuthProvider, AuthUIPreview, SmartlinksAuthUI as FirebaseAuthUI, ProtectedRoute, SmartlinksAuthUI, SmartlinksClaimUI, tokenStorage, useAuth };
|
|
11815
12503
|
//# sourceMappingURL=index.esm.js.map
|