@proveanything/smartlinks-auth-ui 0.1.3 → 0.1.4
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/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 +1 -0
- 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 +804 -116
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +804 -115
- 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 +1 -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
|
+
? '/smartlinks-logo.png' // Default
|
|
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,7 +11411,7 @@ const useAuth = () => {
|
|
|
10819
11411
|
return context;
|
|
10820
11412
|
};
|
|
10821
11413
|
|
|
10822
|
-
const AccountManagement = ({ apiEndpoint, clientId, onProfileUpdated, onEmailChangeRequested, onPasswordChanged, onAccountDeleted, onError, theme = 'light', className = '', customization = {}, }) => {
|
|
11414
|
+
const AccountManagement = ({ apiEndpoint, clientId, onProfileUpdated, onEmailChangeRequested, onPasswordChanged, onAccountDeleted, onError, theme = 'light', className = '', customization = {}, minimal = false, }) => {
|
|
10823
11415
|
const auth = useAuth();
|
|
10824
11416
|
const [loading, setLoading] = useState(false);
|
|
10825
11417
|
const [profile, setProfile] = useState(null);
|
|
@@ -11102,6 +11694,99 @@ const AccountManagement = ({ apiEndpoint, clientId, onProfileUpdated, onEmailCha
|
|
|
11102
11694
|
}, className: "auth-button button-secondary", children: "Cancel" })] })] }))] }))] }));
|
|
11103
11695
|
};
|
|
11104
11696
|
|
|
11697
|
+
const SmartlinksClaimUI = ({ apiEndpoint, clientId, clientName, collectionId, productId, proofId, onClaimSuccess, onClaimError, additionalFields = [], theme = 'light', className = '', minimal = false, customization = {}, }) => {
|
|
11698
|
+
const auth = useAuth();
|
|
11699
|
+
const [claimStep, setClaimStep] = useState(auth.isAuthenticated ? 'questions' : 'auth');
|
|
11700
|
+
const [claimData, setClaimData] = useState({});
|
|
11701
|
+
const [error, setError] = useState();
|
|
11702
|
+
const [loading, setLoading] = useState(false);
|
|
11703
|
+
const handleAuthSuccess = (token, user, accountData) => {
|
|
11704
|
+
// Authentication successful
|
|
11705
|
+
auth.login(token, user, accountData);
|
|
11706
|
+
// If no additional questions, proceed directly to claim
|
|
11707
|
+
if (additionalFields.length === 0) {
|
|
11708
|
+
executeClaim(user);
|
|
11709
|
+
}
|
|
11710
|
+
else {
|
|
11711
|
+
setClaimStep('questions');
|
|
11712
|
+
}
|
|
11713
|
+
};
|
|
11714
|
+
const handleQuestionSubmit = async (e) => {
|
|
11715
|
+
e.preventDefault();
|
|
11716
|
+
// Validate required fields
|
|
11717
|
+
const missingFields = additionalFields
|
|
11718
|
+
.filter(field => field.required && !claimData[field.name])
|
|
11719
|
+
.map(field => field.label);
|
|
11720
|
+
if (missingFields.length > 0) {
|
|
11721
|
+
setError(`Please fill in: ${missingFields.join(', ')}`);
|
|
11722
|
+
return;
|
|
11723
|
+
}
|
|
11724
|
+
// Execute claim with collected data
|
|
11725
|
+
if (auth.user) {
|
|
11726
|
+
executeClaim(auth.user);
|
|
11727
|
+
}
|
|
11728
|
+
};
|
|
11729
|
+
const executeClaim = async (user) => {
|
|
11730
|
+
setClaimStep('claiming');
|
|
11731
|
+
setLoading(true);
|
|
11732
|
+
setError(undefined);
|
|
11733
|
+
try {
|
|
11734
|
+
// Create attestation to claim the proof
|
|
11735
|
+
const response = await smartlinks.attestation.create(collectionId, productId, proofId, {
|
|
11736
|
+
public: {
|
|
11737
|
+
claimed: true,
|
|
11738
|
+
claimedAt: new Date().toISOString(),
|
|
11739
|
+
claimedBy: user.uid,
|
|
11740
|
+
...claimData,
|
|
11741
|
+
},
|
|
11742
|
+
private: {},
|
|
11743
|
+
proof: {},
|
|
11744
|
+
});
|
|
11745
|
+
setClaimStep('success');
|
|
11746
|
+
// Call success callback
|
|
11747
|
+
onClaimSuccess({
|
|
11748
|
+
proofId,
|
|
11749
|
+
user,
|
|
11750
|
+
claimData,
|
|
11751
|
+
attestationId: response.id,
|
|
11752
|
+
});
|
|
11753
|
+
}
|
|
11754
|
+
catch (err) {
|
|
11755
|
+
console.error('Claim error:', err);
|
|
11756
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to claim proof';
|
|
11757
|
+
setError(errorMessage);
|
|
11758
|
+
onClaimError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
11759
|
+
setClaimStep(additionalFields.length > 0 ? 'questions' : 'auth');
|
|
11760
|
+
}
|
|
11761
|
+
finally {
|
|
11762
|
+
setLoading(false);
|
|
11763
|
+
}
|
|
11764
|
+
};
|
|
11765
|
+
const handleFieldChange = (fieldName, value) => {
|
|
11766
|
+
setClaimData(prev => ({
|
|
11767
|
+
...prev,
|
|
11768
|
+
[fieldName]: value,
|
|
11769
|
+
}));
|
|
11770
|
+
};
|
|
11771
|
+
// Render authentication step
|
|
11772
|
+
if (claimStep === 'auth') {
|
|
11773
|
+
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 }) }));
|
|
11774
|
+
}
|
|
11775
|
+
// Render additional questions step
|
|
11776
|
+
if (claimStep === 'questions') {
|
|
11777
|
+
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' })] })] }));
|
|
11778
|
+
}
|
|
11779
|
+
// Render claiming step (loading state)
|
|
11780
|
+
if (claimStep === 'claiming') {
|
|
11781
|
+
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..." })] }));
|
|
11782
|
+
}
|
|
11783
|
+
// Render success step
|
|
11784
|
+
if (claimStep === 'success') {
|
|
11785
|
+
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.' })] }));
|
|
11786
|
+
}
|
|
11787
|
+
return null;
|
|
11788
|
+
};
|
|
11789
|
+
|
|
11105
11790
|
const ProtectedRoute = ({ children, fallback, redirectTo, }) => {
|
|
11106
11791
|
const { isAuthenticated, isLoading } = useAuth();
|
|
11107
11792
|
// Show loading state
|
|
@@ -11120,7 +11805,7 @@ const ProtectedRoute = ({ children, fallback, redirectTo, }) => {
|
|
|
11120
11805
|
return jsx(Fragment, { children: children });
|
|
11121
11806
|
};
|
|
11122
11807
|
|
|
11123
|
-
const AuthUIPreview = ({ customization, enabledProviders = ['email', 'google', 'phone'], providerOrder, emailDisplayMode = 'form', theme = 'light', className, }) => {
|
|
11808
|
+
const AuthUIPreview = ({ customization, enabledProviders = ['email', 'google', 'phone'], providerOrder, emailDisplayMode = 'form', theme = 'light', className, minimal = false, }) => {
|
|
11124
11809
|
const showEmail = enabledProviders.includes('email');
|
|
11125
11810
|
const showGoogle = enabledProviders.includes('google');
|
|
11126
11811
|
const showPhone = enabledProviders.includes('phone');
|
|
@@ -11146,14 +11831,14 @@ const AuthUIPreview = ({ customization, enabledProviders = ['email', 'google', '
|
|
|
11146
11831
|
}
|
|
11147
11832
|
return null;
|
|
11148
11833
|
};
|
|
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)) })) : (
|
|
11834
|
+
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)) })) : (
|
|
11150
11835
|
/* Form mode: show email form first, then other providers */
|
|
11151
11836
|
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
11837
|
};
|
|
11153
11838
|
|
|
11154
11839
|
// Default Smartlinks Google OAuth Client ID (public - safe to expose)
|
|
11155
11840
|
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, }) => {
|
|
11841
|
+
const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAuthSuccess, onAuthError, enabledProviders = ['email', 'google', 'phone'], initialMode = 'login', redirectUrl, theme = 'light', className, customization, skipConfigFetch = false, minimal = false, }) => {
|
|
11157
11842
|
const [mode, setMode] = useState(initialMode);
|
|
11158
11843
|
const [loading, setLoading] = useState(false);
|
|
11159
11844
|
const [error, setError] = useState();
|
|
@@ -11173,21 +11858,24 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
11173
11858
|
// Reinitialize Smartlinks SDK when apiEndpoint changes (for test/dev scenarios)
|
|
11174
11859
|
// IMPORTANT: Preserve bearer token during reinitialization
|
|
11175
11860
|
useEffect(() => {
|
|
11176
|
-
|
|
11177
|
-
|
|
11178
|
-
|
|
11179
|
-
|
|
11180
|
-
|
|
11181
|
-
|
|
11182
|
-
|
|
11183
|
-
|
|
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);
|
|
11861
|
+
const reinitializeWithToken = async () => {
|
|
11862
|
+
if (apiEndpoint) {
|
|
11863
|
+
// Get current token before reinitializing
|
|
11864
|
+
const currentToken = await auth.getToken();
|
|
11865
|
+
smartlinks.initializeApi({
|
|
11866
|
+
baseURL: apiEndpoint,
|
|
11867
|
+
proxyMode: false, // Direct API calls when custom endpoint is provided
|
|
11868
|
+
ngrokSkipBrowserWarning: true,
|
|
11188
11869
|
});
|
|
11870
|
+
// Restore bearer token after reinitialization using auth.verifyToken
|
|
11871
|
+
if (currentToken) {
|
|
11872
|
+
smartlinks.auth.verifyToken(currentToken).catch(err => {
|
|
11873
|
+
console.warn('Failed to restore bearer token after reinit:', err);
|
|
11874
|
+
});
|
|
11875
|
+
}
|
|
11189
11876
|
}
|
|
11190
|
-
}
|
|
11877
|
+
};
|
|
11878
|
+
reinitializeWithToken();
|
|
11191
11879
|
}, [apiEndpoint, auth]);
|
|
11192
11880
|
// Get the effective redirect URL (use prop or default to current page)
|
|
11193
11881
|
const getRedirectUrl = () => {
|
|
@@ -11697,9 +12385,9 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
11697
12385
|
}
|
|
11698
12386
|
};
|
|
11699
12387
|
if (configLoading) {
|
|
11700
|
-
return (jsx(AuthContainer, { theme: theme, className: className, children: jsx("div", { style: { textAlign: 'center', padding: '2rem' }, children: jsx("div", { className: "auth-spinner" }) }) }));
|
|
12388
|
+
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" }) }) }));
|
|
11701
12389
|
}
|
|
11702
|
-
return (jsx(AuthContainer, { theme: theme, className: className, config: config, children: authSuccess ? (jsxs("div", { style: { textAlign: 'center', padding: '2rem' }, children: [jsx("div", { style: {
|
|
12390
|
+
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: {
|
|
11703
12391
|
color: 'var(--auth-primary-color, #4F46E5)',
|
|
11704
12392
|
fontSize: '3rem',
|
|
11705
12393
|
marginBottom: '1rem'
|
|
@@ -11807,9 +12495,9 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
11807
12495
|
setShowResendVerification(false);
|
|
11808
12496
|
setShowRequestNewReset(false);
|
|
11809
12497
|
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 }))] }));
|
|
12498
|
+
}, 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 }))] }));
|
|
11811
12499
|
})() })) })) : null }));
|
|
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
|