@tiquo/dom-package 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -6
- package/dist/index.d.mts +154 -35
- package/dist/index.d.ts +154 -35
- package/dist/index.js +423 -105
- package/dist/index.mjs +415 -105
- package/package.json +5 -3
- package/scripts/postinstall.js +14 -0
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,139 @@
|
|
|
1
|
+
// src/customer-cookie.ts
|
|
2
|
+
var COOKIE_NAME = "tiquo_customer_user_ids";
|
|
3
|
+
var COOKIE_MAX_AGE = 31536e3;
|
|
4
|
+
function isClerkAuthenticated() {
|
|
5
|
+
if (typeof document === "undefined") return false;
|
|
6
|
+
return document.cookie.includes("__clerk_db_jwt") || document.cookie.includes("__session");
|
|
7
|
+
}
|
|
8
|
+
function getCustomerUserIds() {
|
|
9
|
+
if (typeof document === "undefined") return [];
|
|
10
|
+
const match = document.cookie.match(new RegExp(`${COOKIE_NAME}=([^;]+)`));
|
|
11
|
+
if (match) {
|
|
12
|
+
try {
|
|
13
|
+
const decoded = decodeURIComponent(match[1]);
|
|
14
|
+
const parsed = JSON.parse(decoded);
|
|
15
|
+
if (Array.isArray(parsed)) {
|
|
16
|
+
return parsed.filter((id) => typeof id === "string");
|
|
17
|
+
}
|
|
18
|
+
} catch {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
function addCustomerUserId(userId) {
|
|
25
|
+
if (typeof document === "undefined") return;
|
|
26
|
+
if (typeof window === "undefined") return;
|
|
27
|
+
if (isClerkAuthenticated()) {
|
|
28
|
+
console.log("[Customer Cookie] Skipping - Clerk user detected");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (!userId || typeof userId !== "string") {
|
|
32
|
+
console.warn("[Customer Cookie] Invalid userId provided");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const existing = getCustomerUserIds();
|
|
36
|
+
if (existing.includes(userId)) {
|
|
37
|
+
console.log("[Customer Cookie] User ID already in cookie");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
existing.push(userId);
|
|
41
|
+
const hostname = window.location.hostname;
|
|
42
|
+
const isProduction = hostname.endsWith(".tiquo.app") || hostname === "tiquo.app";
|
|
43
|
+
const cookieParts = [
|
|
44
|
+
`${COOKIE_NAME}=${encodeURIComponent(JSON.stringify(existing))}`,
|
|
45
|
+
"path=/",
|
|
46
|
+
`max-age=${COOKIE_MAX_AGE}`,
|
|
47
|
+
"SameSite=Lax"
|
|
48
|
+
];
|
|
49
|
+
if (isProduction) {
|
|
50
|
+
cookieParts.push("domain=.tiquo.app");
|
|
51
|
+
cookieParts.push("Secure");
|
|
52
|
+
}
|
|
53
|
+
document.cookie = cookieParts.join("; ");
|
|
54
|
+
console.log("[Customer Cookie] Added user ID to cookie:", userId);
|
|
55
|
+
}
|
|
56
|
+
var CONVEX_SITE_URL = "https://edge.tiquo.app";
|
|
57
|
+
var cachedEmailData = null;
|
|
58
|
+
var EMAIL_CACHE_TTL = 5 * 60 * 1e3;
|
|
59
|
+
async function getPrefilledEmailFromCookie(organizationId) {
|
|
60
|
+
if (typeof window === "undefined") {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
if (isClerkAuthenticated()) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const userIds = getCustomerUserIds();
|
|
67
|
+
if (userIds.length === 0) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
if (cachedEmailData && cachedEmailData.organizationId === organizationId && Date.now() - cachedEmailData.fetchedAt < EMAIL_CACHE_TTL) {
|
|
71
|
+
return {
|
|
72
|
+
email: cachedEmailData.email,
|
|
73
|
+
firstName: cachedEmailData.firstName,
|
|
74
|
+
lastName: cachedEmailData.lastName
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const response = await fetch(`${CONVEX_SITE_URL}/api/customer-cookie-email`, {
|
|
79
|
+
method: "POST",
|
|
80
|
+
headers: { "Content-Type": "application/json" },
|
|
81
|
+
body: JSON.stringify({ userIds, organizationId })
|
|
82
|
+
});
|
|
83
|
+
if (response.ok) {
|
|
84
|
+
const data = await response.json();
|
|
85
|
+
if (data.success && data.email) {
|
|
86
|
+
cachedEmailData = {
|
|
87
|
+
organizationId,
|
|
88
|
+
email: data.email,
|
|
89
|
+
firstName: data.firstName,
|
|
90
|
+
lastName: data.lastName,
|
|
91
|
+
fetchedAt: Date.now()
|
|
92
|
+
};
|
|
93
|
+
return {
|
|
94
|
+
email: data.email,
|
|
95
|
+
firstName: data.firstName,
|
|
96
|
+
lastName: data.lastName
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error("[Customer Cookie] Failed to get prefilled email:", error);
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function clearCachedEmail() {
|
|
107
|
+
cachedEmailData = null;
|
|
108
|
+
}
|
|
109
|
+
async function trackCustomerPresence(organizationId) {
|
|
110
|
+
if (typeof window === "undefined") {
|
|
111
|
+
return { success: false, error: "Not in browser environment" };
|
|
112
|
+
}
|
|
113
|
+
if (isClerkAuthenticated()) {
|
|
114
|
+
return { success: false, error: "Clerk user detected" };
|
|
115
|
+
}
|
|
116
|
+
const userIds = getCustomerUserIds();
|
|
117
|
+
if (userIds.length === 0) {
|
|
118
|
+
return { success: false, error: "No user IDs in cookie" };
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
const response = await fetch(`${CONVEX_SITE_URL}/api/customer-presence`, {
|
|
122
|
+
method: "POST",
|
|
123
|
+
headers: { "Content-Type": "application/json" },
|
|
124
|
+
body: JSON.stringify({ userIds, organizationId })
|
|
125
|
+
});
|
|
126
|
+
if (response.ok) {
|
|
127
|
+
const data = await response.json();
|
|
128
|
+
return data;
|
|
129
|
+
}
|
|
130
|
+
return { success: false, error: "Failed to track presence" };
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.error("[Customer Cookie] Failed to track presence:", error);
|
|
133
|
+
return { success: false, error: "Network error" };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
1
137
|
// src/index.ts
|
|
2
138
|
var TiquoAuthError = class extends Error {
|
|
3
139
|
constructor(message, code, statusCode) {
|
|
@@ -7,12 +143,32 @@ var TiquoAuthError = class extends Error {
|
|
|
7
143
|
this.name = "TiquoAuthError";
|
|
8
144
|
}
|
|
9
145
|
};
|
|
146
|
+
function decodeJWT(token) {
|
|
147
|
+
try {
|
|
148
|
+
const parts = token.split(".");
|
|
149
|
+
if (parts.length !== 3) return null;
|
|
150
|
+
const payload = parts[1];
|
|
151
|
+
const decoded = atob(payload.replace(/-/g, "+").replace(/_/g, "/"));
|
|
152
|
+
return JSON.parse(decoded);
|
|
153
|
+
} catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function isTokenExpired(token, bufferSeconds = 300) {
|
|
158
|
+
const payload = decodeJWT(token);
|
|
159
|
+
if (!payload) return true;
|
|
160
|
+
const expiresAt = payload.exp * 1e3;
|
|
161
|
+
const bufferMs = bufferSeconds * 1e3;
|
|
162
|
+
return Date.now() > expiresAt - bufferMs;
|
|
163
|
+
}
|
|
10
164
|
var TiquoAuth = class {
|
|
11
165
|
constructor(config) {
|
|
12
|
-
this.
|
|
166
|
+
this.accessToken = null;
|
|
167
|
+
this.refreshToken = null;
|
|
13
168
|
this.session = null;
|
|
14
169
|
this.listeners = /* @__PURE__ */ new Set();
|
|
15
170
|
this.refreshTimer = null;
|
|
171
|
+
this.isRefreshing = false;
|
|
16
172
|
// Multi-tab sync
|
|
17
173
|
this.broadcastChannel = null;
|
|
18
174
|
this.isProcessingTabSync = false;
|
|
@@ -30,14 +186,17 @@ var TiquoAuth = class {
|
|
|
30
186
|
apiEndpoint: config.apiEndpoint || "https://edge.tiquo.app",
|
|
31
187
|
storagePrefix: config.storagePrefix || "tiquo_auth_",
|
|
32
188
|
debug: config.debug || false,
|
|
33
|
-
enableTabSync: config.enableTabSync !== false
|
|
189
|
+
enableTabSync: config.enableTabSync !== false,
|
|
34
190
|
// Default true
|
|
191
|
+
accessToken: config.accessToken,
|
|
192
|
+
refreshToken: config.refreshToken
|
|
35
193
|
};
|
|
36
194
|
this.tabId = this.generateTabId();
|
|
37
195
|
if (this.config.enableTabSync) {
|
|
38
196
|
this.initTabSync();
|
|
39
197
|
}
|
|
40
|
-
this.
|
|
198
|
+
this.checkForInjectedTokens();
|
|
199
|
+
this.restoreTokens();
|
|
41
200
|
}
|
|
42
201
|
// ============================================
|
|
43
202
|
// PUBLIC METHODS
|
|
@@ -82,13 +241,19 @@ var TiquoAuth = class {
|
|
|
82
241
|
throw new TiquoAuthError(error.error || "Invalid OTP", "OTP_VERIFY_FAILED", response.status);
|
|
83
242
|
}
|
|
84
243
|
const result = await response.json();
|
|
85
|
-
this.
|
|
86
|
-
this.
|
|
244
|
+
this.accessToken = result.accessToken;
|
|
245
|
+
this.refreshToken = result.refreshToken;
|
|
246
|
+
this.saveTokens();
|
|
87
247
|
await this.refreshSession();
|
|
248
|
+
if (this.session?.user?.id) {
|
|
249
|
+
addCustomerUserId(this.session.user.id);
|
|
250
|
+
}
|
|
88
251
|
this.broadcastTabSync("LOGIN");
|
|
89
252
|
return {
|
|
90
253
|
success: true,
|
|
91
|
-
|
|
254
|
+
accessToken: result.accessToken,
|
|
255
|
+
refreshToken: result.refreshToken,
|
|
256
|
+
expiresIn: result.expiresIn,
|
|
92
257
|
expiresAt: result.expiresAt,
|
|
93
258
|
isNewUser: result.isNewUser,
|
|
94
259
|
hasCustomer: result.hasCustomer
|
|
@@ -98,10 +263,11 @@ var TiquoAuth = class {
|
|
|
98
263
|
* Get the current authenticated user and customer data
|
|
99
264
|
*/
|
|
100
265
|
async getUser() {
|
|
101
|
-
if (!this.
|
|
266
|
+
if (!this.accessToken) {
|
|
102
267
|
return null;
|
|
103
268
|
}
|
|
104
|
-
|
|
269
|
+
await this.refreshTokenIfNeeded();
|
|
270
|
+
if (this.session) {
|
|
105
271
|
return this.session;
|
|
106
272
|
}
|
|
107
273
|
return this.refreshSession();
|
|
@@ -110,60 +276,49 @@ var TiquoAuth = class {
|
|
|
110
276
|
* Check if user is currently authenticated
|
|
111
277
|
*/
|
|
112
278
|
isAuthenticated() {
|
|
113
|
-
return !!this.
|
|
279
|
+
return !!this.accessToken && !isTokenExpired(this.accessToken, 0);
|
|
114
280
|
}
|
|
115
281
|
/**
|
|
116
282
|
* Update the authenticated customer's profile
|
|
117
283
|
* Only allows updating the logged-in customer's own data
|
|
118
284
|
*/
|
|
119
285
|
async updateProfile(updates) {
|
|
120
|
-
|
|
121
|
-
throw new TiquoAuthError("Not authenticated", "NOT_AUTHENTICATED");
|
|
122
|
-
}
|
|
286
|
+
await this.ensureValidToken();
|
|
123
287
|
this.log("Updating customer profile:", updates);
|
|
124
|
-
const response = await this.request("/api/
|
|
288
|
+
const response = await this.request("/api/client/v1/profile", {
|
|
125
289
|
method: "PATCH",
|
|
126
|
-
body: JSON.stringify(
|
|
127
|
-
publicKey: this.config.publicKey,
|
|
128
|
-
sessionToken: this.sessionToken,
|
|
129
|
-
updates
|
|
130
|
-
})
|
|
290
|
+
body: JSON.stringify(updates)
|
|
131
291
|
});
|
|
132
292
|
if (!response.ok) {
|
|
133
293
|
const error = await response.json().catch(() => ({ error: "Failed to update profile" }));
|
|
134
294
|
throw new TiquoAuthError(error.error || "Failed to update profile", "PROFILE_UPDATE_FAILED", response.status);
|
|
135
295
|
}
|
|
136
296
|
const result = await response.json();
|
|
137
|
-
if (this.session && result.customer) {
|
|
297
|
+
if (this.session && result.data?.customer) {
|
|
138
298
|
this.session = {
|
|
139
299
|
...this.session,
|
|
140
|
-
customer:
|
|
141
|
-
id: result.customer.id,
|
|
142
|
-
firstName: result.customer.firstName,
|
|
143
|
-
lastName: result.customer.lastName,
|
|
144
|
-
displayName: result.customer.displayName,
|
|
145
|
-
customerNumber: result.customer.customerNumber,
|
|
146
|
-
email: result.customer.email,
|
|
147
|
-
phone: result.customer.phone
|
|
148
|
-
}
|
|
300
|
+
customer: result.data.customer
|
|
149
301
|
};
|
|
150
302
|
this.notifyListeners();
|
|
151
303
|
this.broadcastTabSync("SESSION_UPDATE");
|
|
152
304
|
}
|
|
153
|
-
return
|
|
305
|
+
return {
|
|
306
|
+
success: true,
|
|
307
|
+
customer: result.data?.customer
|
|
308
|
+
};
|
|
154
309
|
}
|
|
155
310
|
/**
|
|
156
311
|
* Log out the current user
|
|
157
312
|
*/
|
|
158
313
|
async logout() {
|
|
159
314
|
this.log("Logging out");
|
|
160
|
-
if (this.
|
|
315
|
+
if (this.refreshToken) {
|
|
161
316
|
try {
|
|
162
317
|
await this.request("/api/auth-dom/logout", {
|
|
163
318
|
method: "POST",
|
|
164
319
|
body: JSON.stringify({
|
|
165
320
|
publicKey: this.config.publicKey,
|
|
166
|
-
|
|
321
|
+
refreshToken: this.refreshToken
|
|
167
322
|
})
|
|
168
323
|
});
|
|
169
324
|
} catch (error) {
|
|
@@ -171,7 +326,7 @@ var TiquoAuth = class {
|
|
|
171
326
|
}
|
|
172
327
|
}
|
|
173
328
|
this.broadcastTabSync("LOGOUT");
|
|
174
|
-
this.
|
|
329
|
+
this.clearTokens();
|
|
175
330
|
}
|
|
176
331
|
/**
|
|
177
332
|
* Subscribe to authentication state changes
|
|
@@ -188,12 +343,9 @@ var TiquoAuth = class {
|
|
|
188
343
|
* Only returns orders for the logged-in customer
|
|
189
344
|
*/
|
|
190
345
|
async getOrders(options) {
|
|
191
|
-
|
|
192
|
-
throw new TiquoAuthError("Not authenticated", "NOT_AUTHENTICATED");
|
|
193
|
-
}
|
|
346
|
+
await this.ensureValidToken();
|
|
194
347
|
this.log("Fetching customer orders:", options);
|
|
195
|
-
const url = new URL(`${this.config.apiEndpoint}/api/
|
|
196
|
-
url.searchParams.set("publicKey", this.config.publicKey);
|
|
348
|
+
const url = new URL(`${this.config.apiEndpoint}/api/client/v1/orders`);
|
|
197
349
|
if (options?.limit) {
|
|
198
350
|
url.searchParams.set("limit", options.limit.toString());
|
|
199
351
|
}
|
|
@@ -206,7 +358,7 @@ var TiquoAuth = class {
|
|
|
206
358
|
const response = await fetch(url.toString(), {
|
|
207
359
|
method: "GET",
|
|
208
360
|
headers: {
|
|
209
|
-
"Authorization": `Bearer ${this.
|
|
361
|
+
"Authorization": `Bearer ${this.accessToken}`
|
|
210
362
|
},
|
|
211
363
|
credentials: "include"
|
|
212
364
|
});
|
|
@@ -214,19 +366,17 @@ var TiquoAuth = class {
|
|
|
214
366
|
const error = await response.json().catch(() => ({ error: "Failed to get orders" }));
|
|
215
367
|
throw new TiquoAuthError(error.error || "Failed to get orders", "GET_ORDERS_FAILED", response.status);
|
|
216
368
|
}
|
|
217
|
-
|
|
369
|
+
const result = await response.json();
|
|
370
|
+
return result.data || { orders: [], hasMore: false };
|
|
218
371
|
}
|
|
219
372
|
/**
|
|
220
373
|
* Get the authenticated customer's booking history
|
|
221
374
|
* Only returns bookings for the logged-in customer
|
|
222
375
|
*/
|
|
223
376
|
async getBookings(options) {
|
|
224
|
-
|
|
225
|
-
throw new TiquoAuthError("Not authenticated", "NOT_AUTHENTICATED");
|
|
226
|
-
}
|
|
377
|
+
await this.ensureValidToken();
|
|
227
378
|
this.log("Fetching customer bookings:", options);
|
|
228
|
-
const url = new URL(`${this.config.apiEndpoint}/api/
|
|
229
|
-
url.searchParams.set("publicKey", this.config.publicKey);
|
|
379
|
+
const url = new URL(`${this.config.apiEndpoint}/api/client/v1/bookings`);
|
|
230
380
|
if (options?.limit) {
|
|
231
381
|
url.searchParams.set("limit", options.limit.toString());
|
|
232
382
|
}
|
|
@@ -242,7 +392,7 @@ var TiquoAuth = class {
|
|
|
242
392
|
const response = await fetch(url.toString(), {
|
|
243
393
|
method: "GET",
|
|
244
394
|
headers: {
|
|
245
|
-
"Authorization": `Bearer ${this.
|
|
395
|
+
"Authorization": `Bearer ${this.accessToken}`
|
|
246
396
|
},
|
|
247
397
|
credentials: "include"
|
|
248
398
|
});
|
|
@@ -250,19 +400,17 @@ var TiquoAuth = class {
|
|
|
250
400
|
const error = await response.json().catch(() => ({ error: "Failed to get bookings" }));
|
|
251
401
|
throw new TiquoAuthError(error.error || "Failed to get bookings", "GET_BOOKINGS_FAILED", response.status);
|
|
252
402
|
}
|
|
253
|
-
|
|
403
|
+
const result = await response.json();
|
|
404
|
+
return result.data || { bookings: [], hasMore: false };
|
|
254
405
|
}
|
|
255
406
|
/**
|
|
256
407
|
* Get the authenticated customer's enquiry history
|
|
257
408
|
* Only returns enquiries for the logged-in customer
|
|
258
409
|
*/
|
|
259
410
|
async getEnquiries(options) {
|
|
260
|
-
|
|
261
|
-
throw new TiquoAuthError("Not authenticated", "NOT_AUTHENTICATED");
|
|
262
|
-
}
|
|
411
|
+
await this.ensureValidToken();
|
|
263
412
|
this.log("Fetching customer enquiries:", options);
|
|
264
|
-
const url = new URL(`${this.config.apiEndpoint}/api/
|
|
265
|
-
url.searchParams.set("publicKey", this.config.publicKey);
|
|
413
|
+
const url = new URL(`${this.config.apiEndpoint}/api/client/v1/enquiries`);
|
|
266
414
|
if (options?.limit) {
|
|
267
415
|
url.searchParams.set("limit", options.limit.toString());
|
|
268
416
|
}
|
|
@@ -275,7 +423,7 @@ var TiquoAuth = class {
|
|
|
275
423
|
const response = await fetch(url.toString(), {
|
|
276
424
|
method: "GET",
|
|
277
425
|
headers: {
|
|
278
|
-
"Authorization": `Bearer ${this.
|
|
426
|
+
"Authorization": `Bearer ${this.accessToken}`
|
|
279
427
|
},
|
|
280
428
|
credentials: "include"
|
|
281
429
|
});
|
|
@@ -283,21 +431,20 @@ var TiquoAuth = class {
|
|
|
283
431
|
const error = await response.json().catch(() => ({ error: "Failed to get enquiries" }));
|
|
284
432
|
throw new TiquoAuthError(error.error || "Failed to get enquiries", "GET_ENQUIRIES_FAILED", response.status);
|
|
285
433
|
}
|
|
286
|
-
|
|
434
|
+
const result = await response.json();
|
|
435
|
+
return result.data || { enquiries: [], hasMore: false };
|
|
287
436
|
}
|
|
288
437
|
/**
|
|
289
438
|
* Generate a short-lived token for customer flow iframe authentication
|
|
290
439
|
*/
|
|
291
440
|
async getIframeToken(customerFlowId) {
|
|
292
|
-
|
|
293
|
-
throw new TiquoAuthError("Not authenticated", "NOT_AUTHENTICATED");
|
|
294
|
-
}
|
|
441
|
+
await this.ensureValidToken();
|
|
295
442
|
this.log("Generating iframe token");
|
|
296
443
|
const response = await this.request("/api/auth-dom/iframe-token", {
|
|
297
444
|
method: "POST",
|
|
298
445
|
body: JSON.stringify({
|
|
299
446
|
publicKey: this.config.publicKey,
|
|
300
|
-
sessionToken: this.
|
|
447
|
+
sessionToken: this.accessToken,
|
|
301
448
|
customerFlowId
|
|
302
449
|
})
|
|
303
450
|
});
|
|
@@ -320,7 +467,7 @@ var TiquoAuth = class {
|
|
|
320
467
|
throw new TiquoAuthError("Container element not found", "CONTAINER_NOT_FOUND");
|
|
321
468
|
}
|
|
322
469
|
let authToken;
|
|
323
|
-
if (this.
|
|
470
|
+
if (this.accessToken) {
|
|
324
471
|
try {
|
|
325
472
|
const { token } = await this.getIframeToken();
|
|
326
473
|
authToken = token;
|
|
@@ -350,22 +497,71 @@ var TiquoAuth = class {
|
|
|
350
497
|
return iframe;
|
|
351
498
|
}
|
|
352
499
|
/**
|
|
353
|
-
* Get the current
|
|
500
|
+
* Get the current access token (for advanced use cases)
|
|
501
|
+
*/
|
|
502
|
+
getAccessToken() {
|
|
503
|
+
return this.accessToken;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Get the current refresh token (for advanced use cases)
|
|
354
507
|
*/
|
|
355
|
-
|
|
356
|
-
return this.
|
|
508
|
+
getRefreshToken() {
|
|
509
|
+
return this.refreshToken;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Initialize with external tokens (for WebView integration)
|
|
513
|
+
*/
|
|
514
|
+
initWithTokens(accessToken, refreshToken) {
|
|
515
|
+
this.log("Initializing with external tokens");
|
|
516
|
+
this.accessToken = accessToken;
|
|
517
|
+
this.refreshToken = refreshToken || null;
|
|
518
|
+
this.saveTokens();
|
|
519
|
+
this.refreshSession();
|
|
520
|
+
this.broadcastTabSync("LOGIN");
|
|
357
521
|
}
|
|
358
522
|
// ============================================
|
|
359
523
|
// PRIVATE METHODS
|
|
360
524
|
// ============================================
|
|
525
|
+
/**
|
|
526
|
+
* Check for tokens injected by native apps (WebView integration)
|
|
527
|
+
*/
|
|
528
|
+
checkForInjectedTokens() {
|
|
529
|
+
if (typeof window === "undefined") return;
|
|
530
|
+
if (this.config.accessToken) {
|
|
531
|
+
this.log("Found token in config");
|
|
532
|
+
this.accessToken = this.config.accessToken;
|
|
533
|
+
this.refreshToken = this.config.refreshToken || null;
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
if (window.__TIQUO_INIT_TOKEN__?.accessToken) {
|
|
537
|
+
this.log("Found token in window.__TIQUO_INIT_TOKEN__");
|
|
538
|
+
this.accessToken = window.__TIQUO_INIT_TOKEN__.accessToken;
|
|
539
|
+
this.refreshToken = window.__TIQUO_INIT_TOKEN__.refreshToken || null;
|
|
540
|
+
delete window.__TIQUO_INIT_TOKEN__;
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
if (window.location.hash) {
|
|
544
|
+
const params = new URLSearchParams(window.location.hash.slice(1));
|
|
545
|
+
const accessToken = params.get("access_token");
|
|
546
|
+
if (accessToken) {
|
|
547
|
+
this.log("Found token in URL fragment");
|
|
548
|
+
this.accessToken = accessToken;
|
|
549
|
+
this.refreshToken = params.get("refresh_token");
|
|
550
|
+
if (window.history?.replaceState) {
|
|
551
|
+
window.history.replaceState(null, "", window.location.pathname + window.location.search);
|
|
552
|
+
}
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
361
557
|
async request(path, options) {
|
|
362
558
|
const url = `${this.config.apiEndpoint}${path}`;
|
|
363
559
|
const headers = {
|
|
364
560
|
"Content-Type": "application/json",
|
|
365
561
|
...options.headers
|
|
366
562
|
};
|
|
367
|
-
if (this.
|
|
368
|
-
headers["Authorization"] = `Bearer ${this.
|
|
563
|
+
if (this.accessToken) {
|
|
564
|
+
headers["Authorization"] = `Bearer ${this.accessToken}`;
|
|
369
565
|
}
|
|
370
566
|
return fetch(url, {
|
|
371
567
|
...options,
|
|
@@ -373,73 +569,171 @@ var TiquoAuth = class {
|
|
|
373
569
|
credentials: "include"
|
|
374
570
|
});
|
|
375
571
|
}
|
|
572
|
+
/**
|
|
573
|
+
* Ensure we have a valid access token, refreshing if necessary
|
|
574
|
+
*/
|
|
575
|
+
async ensureValidToken() {
|
|
576
|
+
if (!this.accessToken) {
|
|
577
|
+
throw new TiquoAuthError("Not authenticated", "NOT_AUTHENTICATED");
|
|
578
|
+
}
|
|
579
|
+
await this.refreshTokenIfNeeded();
|
|
580
|
+
if (!this.accessToken) {
|
|
581
|
+
throw new TiquoAuthError("Not authenticated", "NOT_AUTHENTICATED");
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Refresh the access token if it's expired or about to expire
|
|
586
|
+
*/
|
|
587
|
+
async refreshTokenIfNeeded() {
|
|
588
|
+
if (!this.accessToken || !this.refreshToken) {
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
if (!isTokenExpired(this.accessToken, 300)) {
|
|
592
|
+
return true;
|
|
593
|
+
}
|
|
594
|
+
return this.performTokenRefresh();
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Perform the actual token refresh
|
|
598
|
+
*/
|
|
599
|
+
async performTokenRefresh() {
|
|
600
|
+
if (this.isRefreshing) {
|
|
601
|
+
return new Promise((resolve) => {
|
|
602
|
+
const checkInterval = setInterval(() => {
|
|
603
|
+
if (!this.isRefreshing) {
|
|
604
|
+
clearInterval(checkInterval);
|
|
605
|
+
resolve(!!this.accessToken);
|
|
606
|
+
}
|
|
607
|
+
}, 100);
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
if (!this.refreshToken) {
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
613
|
+
this.isRefreshing = true;
|
|
614
|
+
this.log("Refreshing access token");
|
|
615
|
+
try {
|
|
616
|
+
const response = await fetch(`${this.config.apiEndpoint}/api/client/v1/refresh`, {
|
|
617
|
+
method: "POST",
|
|
618
|
+
headers: {
|
|
619
|
+
"Content-Type": "application/json"
|
|
620
|
+
},
|
|
621
|
+
body: JSON.stringify({
|
|
622
|
+
refresh_token: this.refreshToken
|
|
623
|
+
}),
|
|
624
|
+
credentials: "include"
|
|
625
|
+
});
|
|
626
|
+
if (!response.ok) {
|
|
627
|
+
this.log("Token refresh failed, clearing session");
|
|
628
|
+
this.clearTokens();
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
const result = await response.json();
|
|
632
|
+
if (result.success && result.data) {
|
|
633
|
+
this.accessToken = result.data.access_token;
|
|
634
|
+
this.refreshToken = result.data.refresh_token;
|
|
635
|
+
this.saveTokens();
|
|
636
|
+
this.scheduleRefresh();
|
|
637
|
+
this.broadcastTabSync("TOKEN_REFRESH");
|
|
638
|
+
this.log("Token refreshed successfully");
|
|
639
|
+
return true;
|
|
640
|
+
}
|
|
641
|
+
return false;
|
|
642
|
+
} catch (error) {
|
|
643
|
+
this.log("Token refresh error:", error);
|
|
644
|
+
return false;
|
|
645
|
+
} finally {
|
|
646
|
+
this.isRefreshing = false;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
376
649
|
async refreshSession() {
|
|
377
|
-
if (!this.
|
|
650
|
+
if (!this.accessToken) {
|
|
378
651
|
return null;
|
|
379
652
|
}
|
|
380
653
|
try {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
const response = await fetch(url.toString(), {
|
|
654
|
+
await this.refreshTokenIfNeeded();
|
|
655
|
+
const response = await fetch(`${this.config.apiEndpoint}/api/client/v1/profile`, {
|
|
384
656
|
method: "GET",
|
|
385
657
|
headers: {
|
|
386
|
-
"Authorization": `Bearer ${this.
|
|
658
|
+
"Authorization": `Bearer ${this.accessToken}`
|
|
387
659
|
},
|
|
388
660
|
credentials: "include"
|
|
389
661
|
});
|
|
390
662
|
if (!response.ok) {
|
|
391
663
|
this.log("Session invalid, clearing");
|
|
392
|
-
this.
|
|
664
|
+
this.clearTokens();
|
|
393
665
|
return null;
|
|
394
666
|
}
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
667
|
+
const result = await response.json();
|
|
668
|
+
if (result.success && result.data) {
|
|
669
|
+
const payload = decodeJWT(this.accessToken);
|
|
670
|
+
const expiresAt = payload?.exp ? payload.exp * 1e3 : Date.now() + 36e5;
|
|
671
|
+
this.session = {
|
|
672
|
+
user: result.data.user,
|
|
673
|
+
customer: result.data.customer,
|
|
674
|
+
expiresAt
|
|
675
|
+
};
|
|
676
|
+
if (this.session.user?.id) {
|
|
677
|
+
addCustomerUserId(this.session.user.id);
|
|
678
|
+
}
|
|
679
|
+
this.notifyListeners();
|
|
680
|
+
this.scheduleRefresh();
|
|
681
|
+
return this.session;
|
|
682
|
+
}
|
|
683
|
+
return null;
|
|
404
684
|
} catch (error) {
|
|
405
685
|
this.log("Session refresh error:", error);
|
|
406
686
|
return null;
|
|
407
687
|
}
|
|
408
688
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
689
|
+
saveTokens() {
|
|
690
|
+
try {
|
|
691
|
+
if (this.accessToken) {
|
|
412
692
|
localStorage.setItem(
|
|
413
|
-
`${this.config.storagePrefix}
|
|
414
|
-
this.
|
|
693
|
+
`${this.config.storagePrefix}access_token`,
|
|
694
|
+
this.accessToken
|
|
415
695
|
);
|
|
416
|
-
} catch (error) {
|
|
417
|
-
this.log("Failed to save session to storage:", error);
|
|
418
696
|
}
|
|
697
|
+
if (this.refreshToken) {
|
|
698
|
+
localStorage.setItem(
|
|
699
|
+
`${this.config.storagePrefix}refresh_token`,
|
|
700
|
+
this.refreshToken
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
} catch (error) {
|
|
704
|
+
this.log("Failed to save tokens to storage:", error);
|
|
419
705
|
}
|
|
420
706
|
}
|
|
421
|
-
|
|
707
|
+
restoreTokens() {
|
|
708
|
+
if (this.accessToken) {
|
|
709
|
+
this.refreshSession();
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
422
712
|
try {
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
713
|
+
const accessToken = localStorage.getItem(`${this.config.storagePrefix}access_token`);
|
|
714
|
+
const refreshToken = localStorage.getItem(`${this.config.storagePrefix}refresh_token`);
|
|
715
|
+
if (accessToken) {
|
|
716
|
+
this.accessToken = accessToken;
|
|
717
|
+
this.refreshToken = refreshToken;
|
|
426
718
|
this.refreshSession();
|
|
427
719
|
}
|
|
428
720
|
} catch (error) {
|
|
429
|
-
this.log("Failed to restore
|
|
721
|
+
this.log("Failed to restore tokens from storage:", error);
|
|
430
722
|
}
|
|
431
723
|
}
|
|
432
|
-
|
|
433
|
-
this.
|
|
724
|
+
clearTokens() {
|
|
725
|
+
this.accessToken = null;
|
|
726
|
+
this.refreshToken = null;
|
|
434
727
|
this.session = null;
|
|
435
728
|
if (this.refreshTimer) {
|
|
436
729
|
clearTimeout(this.refreshTimer);
|
|
437
730
|
this.refreshTimer = null;
|
|
438
731
|
}
|
|
439
732
|
try {
|
|
440
|
-
localStorage.removeItem(`${this.config.storagePrefix}
|
|
733
|
+
localStorage.removeItem(`${this.config.storagePrefix}access_token`);
|
|
734
|
+
localStorage.removeItem(`${this.config.storagePrefix}refresh_token`);
|
|
441
735
|
} catch (error) {
|
|
442
|
-
this.log("Failed to clear
|
|
736
|
+
this.log("Failed to clear tokens from storage:", error);
|
|
443
737
|
}
|
|
444
738
|
this.notifyListeners();
|
|
445
739
|
}
|
|
@@ -456,11 +750,14 @@ var TiquoAuth = class {
|
|
|
456
750
|
if (this.refreshTimer) {
|
|
457
751
|
clearTimeout(this.refreshTimer);
|
|
458
752
|
}
|
|
459
|
-
if (!this.
|
|
460
|
-
const
|
|
753
|
+
if (!this.accessToken) return;
|
|
754
|
+
const payload = decodeJWT(this.accessToken);
|
|
755
|
+
if (!payload) return;
|
|
756
|
+
const expiresAt = payload.exp * 1e3;
|
|
757
|
+
const refreshIn = expiresAt - Date.now() - 5 * 60 * 1e3;
|
|
461
758
|
if (refreshIn > 0) {
|
|
462
759
|
this.refreshTimer = setTimeout(() => {
|
|
463
|
-
this.
|
|
760
|
+
this.performTokenRefresh();
|
|
464
761
|
}, refreshIn);
|
|
465
762
|
}
|
|
466
763
|
}
|
|
@@ -497,7 +794,8 @@ var TiquoAuth = class {
|
|
|
497
794
|
type,
|
|
498
795
|
tabId: this.tabId,
|
|
499
796
|
timestamp: Date.now(),
|
|
500
|
-
|
|
797
|
+
accessToken: type === "LOGOUT" ? null : this.accessToken,
|
|
798
|
+
refreshToken: type === "LOGOUT" ? null : this.refreshToken,
|
|
501
799
|
session: type === "LOGOUT" ? null : this.session
|
|
502
800
|
};
|
|
503
801
|
try {
|
|
@@ -515,24 +813,30 @@ var TiquoAuth = class {
|
|
|
515
813
|
try {
|
|
516
814
|
switch (message.type) {
|
|
517
815
|
case "LOGIN":
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
this.
|
|
521
|
-
this.
|
|
816
|
+
case "TOKEN_REFRESH":
|
|
817
|
+
if (message.accessToken) {
|
|
818
|
+
this.accessToken = message.accessToken;
|
|
819
|
+
this.refreshToken = message.refreshToken || null;
|
|
820
|
+
if (message.session) {
|
|
821
|
+
this.session = message.session;
|
|
822
|
+
}
|
|
823
|
+
this.saveTokens();
|
|
522
824
|
this.scheduleRefresh();
|
|
523
825
|
this.notifyListeners();
|
|
524
|
-
this.log("
|
|
826
|
+
this.log("Tokens synced from another tab");
|
|
525
827
|
}
|
|
526
828
|
break;
|
|
527
829
|
case "LOGOUT":
|
|
528
|
-
this.
|
|
830
|
+
this.accessToken = null;
|
|
831
|
+
this.refreshToken = null;
|
|
529
832
|
this.session = null;
|
|
530
833
|
if (this.refreshTimer) {
|
|
531
834
|
clearTimeout(this.refreshTimer);
|
|
532
835
|
this.refreshTimer = null;
|
|
533
836
|
}
|
|
534
837
|
try {
|
|
535
|
-
localStorage.removeItem(`${this.config.storagePrefix}
|
|
838
|
+
localStorage.removeItem(`${this.config.storagePrefix}access_token`);
|
|
839
|
+
localStorage.removeItem(`${this.config.storagePrefix}refresh_token`);
|
|
536
840
|
} catch {
|
|
537
841
|
}
|
|
538
842
|
this.notifyListeners();
|
|
@@ -605,6 +909,12 @@ var index_default = TiquoAuth;
|
|
|
605
909
|
export {
|
|
606
910
|
TiquoAuth,
|
|
607
911
|
TiquoAuthError,
|
|
912
|
+
addCustomerUserId,
|
|
913
|
+
clearCachedEmail,
|
|
608
914
|
index_default as default,
|
|
915
|
+
getCustomerUserIds,
|
|
916
|
+
getPrefilledEmailFromCookie,
|
|
917
|
+
isClerkAuthenticated,
|
|
918
|
+
trackCustomerPresence,
|
|
609
919
|
useTiquoAuth
|
|
610
920
|
};
|