@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.js
CHANGED
|
@@ -22,10 +22,154 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
TiquoAuth: () => TiquoAuth,
|
|
24
24
|
TiquoAuthError: () => TiquoAuthError,
|
|
25
|
+
addCustomerUserId: () => addCustomerUserId,
|
|
26
|
+
clearCachedEmail: () => clearCachedEmail,
|
|
25
27
|
default: () => index_default,
|
|
28
|
+
getCustomerUserIds: () => getCustomerUserIds,
|
|
29
|
+
getPrefilledEmailFromCookie: () => getPrefilledEmailFromCookie,
|
|
30
|
+
isClerkAuthenticated: () => isClerkAuthenticated,
|
|
31
|
+
trackCustomerPresence: () => trackCustomerPresence,
|
|
26
32
|
useTiquoAuth: () => useTiquoAuth
|
|
27
33
|
});
|
|
28
34
|
module.exports = __toCommonJS(index_exports);
|
|
35
|
+
|
|
36
|
+
// src/customer-cookie.ts
|
|
37
|
+
var COOKIE_NAME = "tiquo_customer_user_ids";
|
|
38
|
+
var COOKIE_MAX_AGE = 31536e3;
|
|
39
|
+
function isClerkAuthenticated() {
|
|
40
|
+
if (typeof document === "undefined") return false;
|
|
41
|
+
return document.cookie.includes("__clerk_db_jwt") || document.cookie.includes("__session");
|
|
42
|
+
}
|
|
43
|
+
function getCustomerUserIds() {
|
|
44
|
+
if (typeof document === "undefined") return [];
|
|
45
|
+
const match = document.cookie.match(new RegExp(`${COOKIE_NAME}=([^;]+)`));
|
|
46
|
+
if (match) {
|
|
47
|
+
try {
|
|
48
|
+
const decoded = decodeURIComponent(match[1]);
|
|
49
|
+
const parsed = JSON.parse(decoded);
|
|
50
|
+
if (Array.isArray(parsed)) {
|
|
51
|
+
return parsed.filter((id) => typeof id === "string");
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
function addCustomerUserId(userId) {
|
|
60
|
+
if (typeof document === "undefined") return;
|
|
61
|
+
if (typeof window === "undefined") return;
|
|
62
|
+
if (isClerkAuthenticated()) {
|
|
63
|
+
console.log("[Customer Cookie] Skipping - Clerk user detected");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (!userId || typeof userId !== "string") {
|
|
67
|
+
console.warn("[Customer Cookie] Invalid userId provided");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const existing = getCustomerUserIds();
|
|
71
|
+
if (existing.includes(userId)) {
|
|
72
|
+
console.log("[Customer Cookie] User ID already in cookie");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
existing.push(userId);
|
|
76
|
+
const hostname = window.location.hostname;
|
|
77
|
+
const isProduction = hostname.endsWith(".tiquo.app") || hostname === "tiquo.app";
|
|
78
|
+
const cookieParts = [
|
|
79
|
+
`${COOKIE_NAME}=${encodeURIComponent(JSON.stringify(existing))}`,
|
|
80
|
+
"path=/",
|
|
81
|
+
`max-age=${COOKIE_MAX_AGE}`,
|
|
82
|
+
"SameSite=Lax"
|
|
83
|
+
];
|
|
84
|
+
if (isProduction) {
|
|
85
|
+
cookieParts.push("domain=.tiquo.app");
|
|
86
|
+
cookieParts.push("Secure");
|
|
87
|
+
}
|
|
88
|
+
document.cookie = cookieParts.join("; ");
|
|
89
|
+
console.log("[Customer Cookie] Added user ID to cookie:", userId);
|
|
90
|
+
}
|
|
91
|
+
var CONVEX_SITE_URL = "https://edge.tiquo.app";
|
|
92
|
+
var cachedEmailData = null;
|
|
93
|
+
var EMAIL_CACHE_TTL = 5 * 60 * 1e3;
|
|
94
|
+
async function getPrefilledEmailFromCookie(organizationId) {
|
|
95
|
+
if (typeof window === "undefined") {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
if (isClerkAuthenticated()) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
const userIds = getCustomerUserIds();
|
|
102
|
+
if (userIds.length === 0) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
if (cachedEmailData && cachedEmailData.organizationId === organizationId && Date.now() - cachedEmailData.fetchedAt < EMAIL_CACHE_TTL) {
|
|
106
|
+
return {
|
|
107
|
+
email: cachedEmailData.email,
|
|
108
|
+
firstName: cachedEmailData.firstName,
|
|
109
|
+
lastName: cachedEmailData.lastName
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const response = await fetch(`${CONVEX_SITE_URL}/api/customer-cookie-email`, {
|
|
114
|
+
method: "POST",
|
|
115
|
+
headers: { "Content-Type": "application/json" },
|
|
116
|
+
body: JSON.stringify({ userIds, organizationId })
|
|
117
|
+
});
|
|
118
|
+
if (response.ok) {
|
|
119
|
+
const data = await response.json();
|
|
120
|
+
if (data.success && data.email) {
|
|
121
|
+
cachedEmailData = {
|
|
122
|
+
organizationId,
|
|
123
|
+
email: data.email,
|
|
124
|
+
firstName: data.firstName,
|
|
125
|
+
lastName: data.lastName,
|
|
126
|
+
fetchedAt: Date.now()
|
|
127
|
+
};
|
|
128
|
+
return {
|
|
129
|
+
email: data.email,
|
|
130
|
+
firstName: data.firstName,
|
|
131
|
+
lastName: data.lastName
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error("[Customer Cookie] Failed to get prefilled email:", error);
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function clearCachedEmail() {
|
|
142
|
+
cachedEmailData = null;
|
|
143
|
+
}
|
|
144
|
+
async function trackCustomerPresence(organizationId) {
|
|
145
|
+
if (typeof window === "undefined") {
|
|
146
|
+
return { success: false, error: "Not in browser environment" };
|
|
147
|
+
}
|
|
148
|
+
if (isClerkAuthenticated()) {
|
|
149
|
+
return { success: false, error: "Clerk user detected" };
|
|
150
|
+
}
|
|
151
|
+
const userIds = getCustomerUserIds();
|
|
152
|
+
if (userIds.length === 0) {
|
|
153
|
+
return { success: false, error: "No user IDs in cookie" };
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const response = await fetch(`${CONVEX_SITE_URL}/api/customer-presence`, {
|
|
157
|
+
method: "POST",
|
|
158
|
+
headers: { "Content-Type": "application/json" },
|
|
159
|
+
body: JSON.stringify({ userIds, organizationId })
|
|
160
|
+
});
|
|
161
|
+
if (response.ok) {
|
|
162
|
+
const data = await response.json();
|
|
163
|
+
return data;
|
|
164
|
+
}
|
|
165
|
+
return { success: false, error: "Failed to track presence" };
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.error("[Customer Cookie] Failed to track presence:", error);
|
|
168
|
+
return { success: false, error: "Network error" };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/index.ts
|
|
29
173
|
var TiquoAuthError = class extends Error {
|
|
30
174
|
constructor(message, code, statusCode) {
|
|
31
175
|
super(message);
|
|
@@ -34,12 +178,32 @@ var TiquoAuthError = class extends Error {
|
|
|
34
178
|
this.name = "TiquoAuthError";
|
|
35
179
|
}
|
|
36
180
|
};
|
|
181
|
+
function decodeJWT(token) {
|
|
182
|
+
try {
|
|
183
|
+
const parts = token.split(".");
|
|
184
|
+
if (parts.length !== 3) return null;
|
|
185
|
+
const payload = parts[1];
|
|
186
|
+
const decoded = atob(payload.replace(/-/g, "+").replace(/_/g, "/"));
|
|
187
|
+
return JSON.parse(decoded);
|
|
188
|
+
} catch {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function isTokenExpired(token, bufferSeconds = 300) {
|
|
193
|
+
const payload = decodeJWT(token);
|
|
194
|
+
if (!payload) return true;
|
|
195
|
+
const expiresAt = payload.exp * 1e3;
|
|
196
|
+
const bufferMs = bufferSeconds * 1e3;
|
|
197
|
+
return Date.now() > expiresAt - bufferMs;
|
|
198
|
+
}
|
|
37
199
|
var TiquoAuth = class {
|
|
38
200
|
constructor(config) {
|
|
39
|
-
this.
|
|
201
|
+
this.accessToken = null;
|
|
202
|
+
this.refreshToken = null;
|
|
40
203
|
this.session = null;
|
|
41
204
|
this.listeners = /* @__PURE__ */ new Set();
|
|
42
205
|
this.refreshTimer = null;
|
|
206
|
+
this.isRefreshing = false;
|
|
43
207
|
// Multi-tab sync
|
|
44
208
|
this.broadcastChannel = null;
|
|
45
209
|
this.isProcessingTabSync = false;
|
|
@@ -57,14 +221,17 @@ var TiquoAuth = class {
|
|
|
57
221
|
apiEndpoint: config.apiEndpoint || "https://edge.tiquo.app",
|
|
58
222
|
storagePrefix: config.storagePrefix || "tiquo_auth_",
|
|
59
223
|
debug: config.debug || false,
|
|
60
|
-
enableTabSync: config.enableTabSync !== false
|
|
224
|
+
enableTabSync: config.enableTabSync !== false,
|
|
61
225
|
// Default true
|
|
226
|
+
accessToken: config.accessToken,
|
|
227
|
+
refreshToken: config.refreshToken
|
|
62
228
|
};
|
|
63
229
|
this.tabId = this.generateTabId();
|
|
64
230
|
if (this.config.enableTabSync) {
|
|
65
231
|
this.initTabSync();
|
|
66
232
|
}
|
|
67
|
-
this.
|
|
233
|
+
this.checkForInjectedTokens();
|
|
234
|
+
this.restoreTokens();
|
|
68
235
|
}
|
|
69
236
|
// ============================================
|
|
70
237
|
// PUBLIC METHODS
|
|
@@ -109,13 +276,19 @@ var TiquoAuth = class {
|
|
|
109
276
|
throw new TiquoAuthError(error.error || "Invalid OTP", "OTP_VERIFY_FAILED", response.status);
|
|
110
277
|
}
|
|
111
278
|
const result = await response.json();
|
|
112
|
-
this.
|
|
113
|
-
this.
|
|
279
|
+
this.accessToken = result.accessToken;
|
|
280
|
+
this.refreshToken = result.refreshToken;
|
|
281
|
+
this.saveTokens();
|
|
114
282
|
await this.refreshSession();
|
|
283
|
+
if (this.session?.user?.id) {
|
|
284
|
+
addCustomerUserId(this.session.user.id);
|
|
285
|
+
}
|
|
115
286
|
this.broadcastTabSync("LOGIN");
|
|
116
287
|
return {
|
|
117
288
|
success: true,
|
|
118
|
-
|
|
289
|
+
accessToken: result.accessToken,
|
|
290
|
+
refreshToken: result.refreshToken,
|
|
291
|
+
expiresIn: result.expiresIn,
|
|
119
292
|
expiresAt: result.expiresAt,
|
|
120
293
|
isNewUser: result.isNewUser,
|
|
121
294
|
hasCustomer: result.hasCustomer
|
|
@@ -125,10 +298,11 @@ var TiquoAuth = class {
|
|
|
125
298
|
* Get the current authenticated user and customer data
|
|
126
299
|
*/
|
|
127
300
|
async getUser() {
|
|
128
|
-
if (!this.
|
|
301
|
+
if (!this.accessToken) {
|
|
129
302
|
return null;
|
|
130
303
|
}
|
|
131
|
-
|
|
304
|
+
await this.refreshTokenIfNeeded();
|
|
305
|
+
if (this.session) {
|
|
132
306
|
return this.session;
|
|
133
307
|
}
|
|
134
308
|
return this.refreshSession();
|
|
@@ -137,60 +311,49 @@ var TiquoAuth = class {
|
|
|
137
311
|
* Check if user is currently authenticated
|
|
138
312
|
*/
|
|
139
313
|
isAuthenticated() {
|
|
140
|
-
return !!this.
|
|
314
|
+
return !!this.accessToken && !isTokenExpired(this.accessToken, 0);
|
|
141
315
|
}
|
|
142
316
|
/**
|
|
143
317
|
* Update the authenticated customer's profile
|
|
144
318
|
* Only allows updating the logged-in customer's own data
|
|
145
319
|
*/
|
|
146
320
|
async updateProfile(updates) {
|
|
147
|
-
|
|
148
|
-
throw new TiquoAuthError("Not authenticated", "NOT_AUTHENTICATED");
|
|
149
|
-
}
|
|
321
|
+
await this.ensureValidToken();
|
|
150
322
|
this.log("Updating customer profile:", updates);
|
|
151
|
-
const response = await this.request("/api/
|
|
323
|
+
const response = await this.request("/api/client/v1/profile", {
|
|
152
324
|
method: "PATCH",
|
|
153
|
-
body: JSON.stringify(
|
|
154
|
-
publicKey: this.config.publicKey,
|
|
155
|
-
sessionToken: this.sessionToken,
|
|
156
|
-
updates
|
|
157
|
-
})
|
|
325
|
+
body: JSON.stringify(updates)
|
|
158
326
|
});
|
|
159
327
|
if (!response.ok) {
|
|
160
328
|
const error = await response.json().catch(() => ({ error: "Failed to update profile" }));
|
|
161
329
|
throw new TiquoAuthError(error.error || "Failed to update profile", "PROFILE_UPDATE_FAILED", response.status);
|
|
162
330
|
}
|
|
163
331
|
const result = await response.json();
|
|
164
|
-
if (this.session && result.customer) {
|
|
332
|
+
if (this.session && result.data?.customer) {
|
|
165
333
|
this.session = {
|
|
166
334
|
...this.session,
|
|
167
|
-
customer:
|
|
168
|
-
id: result.customer.id,
|
|
169
|
-
firstName: result.customer.firstName,
|
|
170
|
-
lastName: result.customer.lastName,
|
|
171
|
-
displayName: result.customer.displayName,
|
|
172
|
-
customerNumber: result.customer.customerNumber,
|
|
173
|
-
email: result.customer.email,
|
|
174
|
-
phone: result.customer.phone
|
|
175
|
-
}
|
|
335
|
+
customer: result.data.customer
|
|
176
336
|
};
|
|
177
337
|
this.notifyListeners();
|
|
178
338
|
this.broadcastTabSync("SESSION_UPDATE");
|
|
179
339
|
}
|
|
180
|
-
return
|
|
340
|
+
return {
|
|
341
|
+
success: true,
|
|
342
|
+
customer: result.data?.customer
|
|
343
|
+
};
|
|
181
344
|
}
|
|
182
345
|
/**
|
|
183
346
|
* Log out the current user
|
|
184
347
|
*/
|
|
185
348
|
async logout() {
|
|
186
349
|
this.log("Logging out");
|
|
187
|
-
if (this.
|
|
350
|
+
if (this.refreshToken) {
|
|
188
351
|
try {
|
|
189
352
|
await this.request("/api/auth-dom/logout", {
|
|
190
353
|
method: "POST",
|
|
191
354
|
body: JSON.stringify({
|
|
192
355
|
publicKey: this.config.publicKey,
|
|
193
|
-
|
|
356
|
+
refreshToken: this.refreshToken
|
|
194
357
|
})
|
|
195
358
|
});
|
|
196
359
|
} catch (error) {
|
|
@@ -198,7 +361,7 @@ var TiquoAuth = class {
|
|
|
198
361
|
}
|
|
199
362
|
}
|
|
200
363
|
this.broadcastTabSync("LOGOUT");
|
|
201
|
-
this.
|
|
364
|
+
this.clearTokens();
|
|
202
365
|
}
|
|
203
366
|
/**
|
|
204
367
|
* Subscribe to authentication state changes
|
|
@@ -215,12 +378,9 @@ var TiquoAuth = class {
|
|
|
215
378
|
* Only returns orders for the logged-in customer
|
|
216
379
|
*/
|
|
217
380
|
async getOrders(options) {
|
|
218
|
-
|
|
219
|
-
throw new TiquoAuthError("Not authenticated", "NOT_AUTHENTICATED");
|
|
220
|
-
}
|
|
381
|
+
await this.ensureValidToken();
|
|
221
382
|
this.log("Fetching customer orders:", options);
|
|
222
|
-
const url = new URL(`${this.config.apiEndpoint}/api/
|
|
223
|
-
url.searchParams.set("publicKey", this.config.publicKey);
|
|
383
|
+
const url = new URL(`${this.config.apiEndpoint}/api/client/v1/orders`);
|
|
224
384
|
if (options?.limit) {
|
|
225
385
|
url.searchParams.set("limit", options.limit.toString());
|
|
226
386
|
}
|
|
@@ -233,7 +393,7 @@ var TiquoAuth = class {
|
|
|
233
393
|
const response = await fetch(url.toString(), {
|
|
234
394
|
method: "GET",
|
|
235
395
|
headers: {
|
|
236
|
-
"Authorization": `Bearer ${this.
|
|
396
|
+
"Authorization": `Bearer ${this.accessToken}`
|
|
237
397
|
},
|
|
238
398
|
credentials: "include"
|
|
239
399
|
});
|
|
@@ -241,19 +401,17 @@ var TiquoAuth = class {
|
|
|
241
401
|
const error = await response.json().catch(() => ({ error: "Failed to get orders" }));
|
|
242
402
|
throw new TiquoAuthError(error.error || "Failed to get orders", "GET_ORDERS_FAILED", response.status);
|
|
243
403
|
}
|
|
244
|
-
|
|
404
|
+
const result = await response.json();
|
|
405
|
+
return result.data || { orders: [], hasMore: false };
|
|
245
406
|
}
|
|
246
407
|
/**
|
|
247
408
|
* Get the authenticated customer's booking history
|
|
248
409
|
* Only returns bookings for the logged-in customer
|
|
249
410
|
*/
|
|
250
411
|
async getBookings(options) {
|
|
251
|
-
|
|
252
|
-
throw new TiquoAuthError("Not authenticated", "NOT_AUTHENTICATED");
|
|
253
|
-
}
|
|
412
|
+
await this.ensureValidToken();
|
|
254
413
|
this.log("Fetching customer bookings:", options);
|
|
255
|
-
const url = new URL(`${this.config.apiEndpoint}/api/
|
|
256
|
-
url.searchParams.set("publicKey", this.config.publicKey);
|
|
414
|
+
const url = new URL(`${this.config.apiEndpoint}/api/client/v1/bookings`);
|
|
257
415
|
if (options?.limit) {
|
|
258
416
|
url.searchParams.set("limit", options.limit.toString());
|
|
259
417
|
}
|
|
@@ -269,7 +427,7 @@ var TiquoAuth = class {
|
|
|
269
427
|
const response = await fetch(url.toString(), {
|
|
270
428
|
method: "GET",
|
|
271
429
|
headers: {
|
|
272
|
-
"Authorization": `Bearer ${this.
|
|
430
|
+
"Authorization": `Bearer ${this.accessToken}`
|
|
273
431
|
},
|
|
274
432
|
credentials: "include"
|
|
275
433
|
});
|
|
@@ -277,19 +435,17 @@ var TiquoAuth = class {
|
|
|
277
435
|
const error = await response.json().catch(() => ({ error: "Failed to get bookings" }));
|
|
278
436
|
throw new TiquoAuthError(error.error || "Failed to get bookings", "GET_BOOKINGS_FAILED", response.status);
|
|
279
437
|
}
|
|
280
|
-
|
|
438
|
+
const result = await response.json();
|
|
439
|
+
return result.data || { bookings: [], hasMore: false };
|
|
281
440
|
}
|
|
282
441
|
/**
|
|
283
442
|
* Get the authenticated customer's enquiry history
|
|
284
443
|
* Only returns enquiries for the logged-in customer
|
|
285
444
|
*/
|
|
286
445
|
async getEnquiries(options) {
|
|
287
|
-
|
|
288
|
-
throw new TiquoAuthError("Not authenticated", "NOT_AUTHENTICATED");
|
|
289
|
-
}
|
|
446
|
+
await this.ensureValidToken();
|
|
290
447
|
this.log("Fetching customer enquiries:", options);
|
|
291
|
-
const url = new URL(`${this.config.apiEndpoint}/api/
|
|
292
|
-
url.searchParams.set("publicKey", this.config.publicKey);
|
|
448
|
+
const url = new URL(`${this.config.apiEndpoint}/api/client/v1/enquiries`);
|
|
293
449
|
if (options?.limit) {
|
|
294
450
|
url.searchParams.set("limit", options.limit.toString());
|
|
295
451
|
}
|
|
@@ -302,7 +458,7 @@ var TiquoAuth = class {
|
|
|
302
458
|
const response = await fetch(url.toString(), {
|
|
303
459
|
method: "GET",
|
|
304
460
|
headers: {
|
|
305
|
-
"Authorization": `Bearer ${this.
|
|
461
|
+
"Authorization": `Bearer ${this.accessToken}`
|
|
306
462
|
},
|
|
307
463
|
credentials: "include"
|
|
308
464
|
});
|
|
@@ -310,21 +466,20 @@ var TiquoAuth = class {
|
|
|
310
466
|
const error = await response.json().catch(() => ({ error: "Failed to get enquiries" }));
|
|
311
467
|
throw new TiquoAuthError(error.error || "Failed to get enquiries", "GET_ENQUIRIES_FAILED", response.status);
|
|
312
468
|
}
|
|
313
|
-
|
|
469
|
+
const result = await response.json();
|
|
470
|
+
return result.data || { enquiries: [], hasMore: false };
|
|
314
471
|
}
|
|
315
472
|
/**
|
|
316
473
|
* Generate a short-lived token for customer flow iframe authentication
|
|
317
474
|
*/
|
|
318
475
|
async getIframeToken(customerFlowId) {
|
|
319
|
-
|
|
320
|
-
throw new TiquoAuthError("Not authenticated", "NOT_AUTHENTICATED");
|
|
321
|
-
}
|
|
476
|
+
await this.ensureValidToken();
|
|
322
477
|
this.log("Generating iframe token");
|
|
323
478
|
const response = await this.request("/api/auth-dom/iframe-token", {
|
|
324
479
|
method: "POST",
|
|
325
480
|
body: JSON.stringify({
|
|
326
481
|
publicKey: this.config.publicKey,
|
|
327
|
-
sessionToken: this.
|
|
482
|
+
sessionToken: this.accessToken,
|
|
328
483
|
customerFlowId
|
|
329
484
|
})
|
|
330
485
|
});
|
|
@@ -347,7 +502,7 @@ var TiquoAuth = class {
|
|
|
347
502
|
throw new TiquoAuthError("Container element not found", "CONTAINER_NOT_FOUND");
|
|
348
503
|
}
|
|
349
504
|
let authToken;
|
|
350
|
-
if (this.
|
|
505
|
+
if (this.accessToken) {
|
|
351
506
|
try {
|
|
352
507
|
const { token } = await this.getIframeToken();
|
|
353
508
|
authToken = token;
|
|
@@ -377,22 +532,71 @@ var TiquoAuth = class {
|
|
|
377
532
|
return iframe;
|
|
378
533
|
}
|
|
379
534
|
/**
|
|
380
|
-
* Get the current
|
|
535
|
+
* Get the current access token (for advanced use cases)
|
|
536
|
+
*/
|
|
537
|
+
getAccessToken() {
|
|
538
|
+
return this.accessToken;
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Get the current refresh token (for advanced use cases)
|
|
381
542
|
*/
|
|
382
|
-
|
|
383
|
-
return this.
|
|
543
|
+
getRefreshToken() {
|
|
544
|
+
return this.refreshToken;
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Initialize with external tokens (for WebView integration)
|
|
548
|
+
*/
|
|
549
|
+
initWithTokens(accessToken, refreshToken) {
|
|
550
|
+
this.log("Initializing with external tokens");
|
|
551
|
+
this.accessToken = accessToken;
|
|
552
|
+
this.refreshToken = refreshToken || null;
|
|
553
|
+
this.saveTokens();
|
|
554
|
+
this.refreshSession();
|
|
555
|
+
this.broadcastTabSync("LOGIN");
|
|
384
556
|
}
|
|
385
557
|
// ============================================
|
|
386
558
|
// PRIVATE METHODS
|
|
387
559
|
// ============================================
|
|
560
|
+
/**
|
|
561
|
+
* Check for tokens injected by native apps (WebView integration)
|
|
562
|
+
*/
|
|
563
|
+
checkForInjectedTokens() {
|
|
564
|
+
if (typeof window === "undefined") return;
|
|
565
|
+
if (this.config.accessToken) {
|
|
566
|
+
this.log("Found token in config");
|
|
567
|
+
this.accessToken = this.config.accessToken;
|
|
568
|
+
this.refreshToken = this.config.refreshToken || null;
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
if (window.__TIQUO_INIT_TOKEN__?.accessToken) {
|
|
572
|
+
this.log("Found token in window.__TIQUO_INIT_TOKEN__");
|
|
573
|
+
this.accessToken = window.__TIQUO_INIT_TOKEN__.accessToken;
|
|
574
|
+
this.refreshToken = window.__TIQUO_INIT_TOKEN__.refreshToken || null;
|
|
575
|
+
delete window.__TIQUO_INIT_TOKEN__;
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
if (window.location.hash) {
|
|
579
|
+
const params = new URLSearchParams(window.location.hash.slice(1));
|
|
580
|
+
const accessToken = params.get("access_token");
|
|
581
|
+
if (accessToken) {
|
|
582
|
+
this.log("Found token in URL fragment");
|
|
583
|
+
this.accessToken = accessToken;
|
|
584
|
+
this.refreshToken = params.get("refresh_token");
|
|
585
|
+
if (window.history?.replaceState) {
|
|
586
|
+
window.history.replaceState(null, "", window.location.pathname + window.location.search);
|
|
587
|
+
}
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
388
592
|
async request(path, options) {
|
|
389
593
|
const url = `${this.config.apiEndpoint}${path}`;
|
|
390
594
|
const headers = {
|
|
391
595
|
"Content-Type": "application/json",
|
|
392
596
|
...options.headers
|
|
393
597
|
};
|
|
394
|
-
if (this.
|
|
395
|
-
headers["Authorization"] = `Bearer ${this.
|
|
598
|
+
if (this.accessToken) {
|
|
599
|
+
headers["Authorization"] = `Bearer ${this.accessToken}`;
|
|
396
600
|
}
|
|
397
601
|
return fetch(url, {
|
|
398
602
|
...options,
|
|
@@ -400,73 +604,171 @@ var TiquoAuth = class {
|
|
|
400
604
|
credentials: "include"
|
|
401
605
|
});
|
|
402
606
|
}
|
|
607
|
+
/**
|
|
608
|
+
* Ensure we have a valid access token, refreshing if necessary
|
|
609
|
+
*/
|
|
610
|
+
async ensureValidToken() {
|
|
611
|
+
if (!this.accessToken) {
|
|
612
|
+
throw new TiquoAuthError("Not authenticated", "NOT_AUTHENTICATED");
|
|
613
|
+
}
|
|
614
|
+
await this.refreshTokenIfNeeded();
|
|
615
|
+
if (!this.accessToken) {
|
|
616
|
+
throw new TiquoAuthError("Not authenticated", "NOT_AUTHENTICATED");
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Refresh the access token if it's expired or about to expire
|
|
621
|
+
*/
|
|
622
|
+
async refreshTokenIfNeeded() {
|
|
623
|
+
if (!this.accessToken || !this.refreshToken) {
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
if (!isTokenExpired(this.accessToken, 300)) {
|
|
627
|
+
return true;
|
|
628
|
+
}
|
|
629
|
+
return this.performTokenRefresh();
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Perform the actual token refresh
|
|
633
|
+
*/
|
|
634
|
+
async performTokenRefresh() {
|
|
635
|
+
if (this.isRefreshing) {
|
|
636
|
+
return new Promise((resolve) => {
|
|
637
|
+
const checkInterval = setInterval(() => {
|
|
638
|
+
if (!this.isRefreshing) {
|
|
639
|
+
clearInterval(checkInterval);
|
|
640
|
+
resolve(!!this.accessToken);
|
|
641
|
+
}
|
|
642
|
+
}, 100);
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
if (!this.refreshToken) {
|
|
646
|
+
return false;
|
|
647
|
+
}
|
|
648
|
+
this.isRefreshing = true;
|
|
649
|
+
this.log("Refreshing access token");
|
|
650
|
+
try {
|
|
651
|
+
const response = await fetch(`${this.config.apiEndpoint}/api/client/v1/refresh`, {
|
|
652
|
+
method: "POST",
|
|
653
|
+
headers: {
|
|
654
|
+
"Content-Type": "application/json"
|
|
655
|
+
},
|
|
656
|
+
body: JSON.stringify({
|
|
657
|
+
refresh_token: this.refreshToken
|
|
658
|
+
}),
|
|
659
|
+
credentials: "include"
|
|
660
|
+
});
|
|
661
|
+
if (!response.ok) {
|
|
662
|
+
this.log("Token refresh failed, clearing session");
|
|
663
|
+
this.clearTokens();
|
|
664
|
+
return false;
|
|
665
|
+
}
|
|
666
|
+
const result = await response.json();
|
|
667
|
+
if (result.success && result.data) {
|
|
668
|
+
this.accessToken = result.data.access_token;
|
|
669
|
+
this.refreshToken = result.data.refresh_token;
|
|
670
|
+
this.saveTokens();
|
|
671
|
+
this.scheduleRefresh();
|
|
672
|
+
this.broadcastTabSync("TOKEN_REFRESH");
|
|
673
|
+
this.log("Token refreshed successfully");
|
|
674
|
+
return true;
|
|
675
|
+
}
|
|
676
|
+
return false;
|
|
677
|
+
} catch (error) {
|
|
678
|
+
this.log("Token refresh error:", error);
|
|
679
|
+
return false;
|
|
680
|
+
} finally {
|
|
681
|
+
this.isRefreshing = false;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
403
684
|
async refreshSession() {
|
|
404
|
-
if (!this.
|
|
685
|
+
if (!this.accessToken) {
|
|
405
686
|
return null;
|
|
406
687
|
}
|
|
407
688
|
try {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
const response = await fetch(url.toString(), {
|
|
689
|
+
await this.refreshTokenIfNeeded();
|
|
690
|
+
const response = await fetch(`${this.config.apiEndpoint}/api/client/v1/profile`, {
|
|
411
691
|
method: "GET",
|
|
412
692
|
headers: {
|
|
413
|
-
"Authorization": `Bearer ${this.
|
|
693
|
+
"Authorization": `Bearer ${this.accessToken}`
|
|
414
694
|
},
|
|
415
695
|
credentials: "include"
|
|
416
696
|
});
|
|
417
697
|
if (!response.ok) {
|
|
418
698
|
this.log("Session invalid, clearing");
|
|
419
|
-
this.
|
|
699
|
+
this.clearTokens();
|
|
420
700
|
return null;
|
|
421
701
|
}
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
702
|
+
const result = await response.json();
|
|
703
|
+
if (result.success && result.data) {
|
|
704
|
+
const payload = decodeJWT(this.accessToken);
|
|
705
|
+
const expiresAt = payload?.exp ? payload.exp * 1e3 : Date.now() + 36e5;
|
|
706
|
+
this.session = {
|
|
707
|
+
user: result.data.user,
|
|
708
|
+
customer: result.data.customer,
|
|
709
|
+
expiresAt
|
|
710
|
+
};
|
|
711
|
+
if (this.session.user?.id) {
|
|
712
|
+
addCustomerUserId(this.session.user.id);
|
|
713
|
+
}
|
|
714
|
+
this.notifyListeners();
|
|
715
|
+
this.scheduleRefresh();
|
|
716
|
+
return this.session;
|
|
717
|
+
}
|
|
718
|
+
return null;
|
|
431
719
|
} catch (error) {
|
|
432
720
|
this.log("Session refresh error:", error);
|
|
433
721
|
return null;
|
|
434
722
|
}
|
|
435
723
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
724
|
+
saveTokens() {
|
|
725
|
+
try {
|
|
726
|
+
if (this.accessToken) {
|
|
439
727
|
localStorage.setItem(
|
|
440
|
-
`${this.config.storagePrefix}
|
|
441
|
-
this.
|
|
728
|
+
`${this.config.storagePrefix}access_token`,
|
|
729
|
+
this.accessToken
|
|
442
730
|
);
|
|
443
|
-
} catch (error) {
|
|
444
|
-
this.log("Failed to save session to storage:", error);
|
|
445
731
|
}
|
|
732
|
+
if (this.refreshToken) {
|
|
733
|
+
localStorage.setItem(
|
|
734
|
+
`${this.config.storagePrefix}refresh_token`,
|
|
735
|
+
this.refreshToken
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
} catch (error) {
|
|
739
|
+
this.log("Failed to save tokens to storage:", error);
|
|
446
740
|
}
|
|
447
741
|
}
|
|
448
|
-
|
|
742
|
+
restoreTokens() {
|
|
743
|
+
if (this.accessToken) {
|
|
744
|
+
this.refreshSession();
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
449
747
|
try {
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
748
|
+
const accessToken = localStorage.getItem(`${this.config.storagePrefix}access_token`);
|
|
749
|
+
const refreshToken = localStorage.getItem(`${this.config.storagePrefix}refresh_token`);
|
|
750
|
+
if (accessToken) {
|
|
751
|
+
this.accessToken = accessToken;
|
|
752
|
+
this.refreshToken = refreshToken;
|
|
453
753
|
this.refreshSession();
|
|
454
754
|
}
|
|
455
755
|
} catch (error) {
|
|
456
|
-
this.log("Failed to restore
|
|
756
|
+
this.log("Failed to restore tokens from storage:", error);
|
|
457
757
|
}
|
|
458
758
|
}
|
|
459
|
-
|
|
460
|
-
this.
|
|
759
|
+
clearTokens() {
|
|
760
|
+
this.accessToken = null;
|
|
761
|
+
this.refreshToken = null;
|
|
461
762
|
this.session = null;
|
|
462
763
|
if (this.refreshTimer) {
|
|
463
764
|
clearTimeout(this.refreshTimer);
|
|
464
765
|
this.refreshTimer = null;
|
|
465
766
|
}
|
|
466
767
|
try {
|
|
467
|
-
localStorage.removeItem(`${this.config.storagePrefix}
|
|
768
|
+
localStorage.removeItem(`${this.config.storagePrefix}access_token`);
|
|
769
|
+
localStorage.removeItem(`${this.config.storagePrefix}refresh_token`);
|
|
468
770
|
} catch (error) {
|
|
469
|
-
this.log("Failed to clear
|
|
771
|
+
this.log("Failed to clear tokens from storage:", error);
|
|
470
772
|
}
|
|
471
773
|
this.notifyListeners();
|
|
472
774
|
}
|
|
@@ -483,11 +785,14 @@ var TiquoAuth = class {
|
|
|
483
785
|
if (this.refreshTimer) {
|
|
484
786
|
clearTimeout(this.refreshTimer);
|
|
485
787
|
}
|
|
486
|
-
if (!this.
|
|
487
|
-
const
|
|
788
|
+
if (!this.accessToken) return;
|
|
789
|
+
const payload = decodeJWT(this.accessToken);
|
|
790
|
+
if (!payload) return;
|
|
791
|
+
const expiresAt = payload.exp * 1e3;
|
|
792
|
+
const refreshIn = expiresAt - Date.now() - 5 * 60 * 1e3;
|
|
488
793
|
if (refreshIn > 0) {
|
|
489
794
|
this.refreshTimer = setTimeout(() => {
|
|
490
|
-
this.
|
|
795
|
+
this.performTokenRefresh();
|
|
491
796
|
}, refreshIn);
|
|
492
797
|
}
|
|
493
798
|
}
|
|
@@ -524,7 +829,8 @@ var TiquoAuth = class {
|
|
|
524
829
|
type,
|
|
525
830
|
tabId: this.tabId,
|
|
526
831
|
timestamp: Date.now(),
|
|
527
|
-
|
|
832
|
+
accessToken: type === "LOGOUT" ? null : this.accessToken,
|
|
833
|
+
refreshToken: type === "LOGOUT" ? null : this.refreshToken,
|
|
528
834
|
session: type === "LOGOUT" ? null : this.session
|
|
529
835
|
};
|
|
530
836
|
try {
|
|
@@ -542,24 +848,30 @@ var TiquoAuth = class {
|
|
|
542
848
|
try {
|
|
543
849
|
switch (message.type) {
|
|
544
850
|
case "LOGIN":
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
this.
|
|
548
|
-
this.
|
|
851
|
+
case "TOKEN_REFRESH":
|
|
852
|
+
if (message.accessToken) {
|
|
853
|
+
this.accessToken = message.accessToken;
|
|
854
|
+
this.refreshToken = message.refreshToken || null;
|
|
855
|
+
if (message.session) {
|
|
856
|
+
this.session = message.session;
|
|
857
|
+
}
|
|
858
|
+
this.saveTokens();
|
|
549
859
|
this.scheduleRefresh();
|
|
550
860
|
this.notifyListeners();
|
|
551
|
-
this.log("
|
|
861
|
+
this.log("Tokens synced from another tab");
|
|
552
862
|
}
|
|
553
863
|
break;
|
|
554
864
|
case "LOGOUT":
|
|
555
|
-
this.
|
|
865
|
+
this.accessToken = null;
|
|
866
|
+
this.refreshToken = null;
|
|
556
867
|
this.session = null;
|
|
557
868
|
if (this.refreshTimer) {
|
|
558
869
|
clearTimeout(this.refreshTimer);
|
|
559
870
|
this.refreshTimer = null;
|
|
560
871
|
}
|
|
561
872
|
try {
|
|
562
|
-
localStorage.removeItem(`${this.config.storagePrefix}
|
|
873
|
+
localStorage.removeItem(`${this.config.storagePrefix}access_token`);
|
|
874
|
+
localStorage.removeItem(`${this.config.storagePrefix}refresh_token`);
|
|
563
875
|
} catch {
|
|
564
876
|
}
|
|
565
877
|
this.notifyListeners();
|
|
@@ -633,5 +945,11 @@ var index_default = TiquoAuth;
|
|
|
633
945
|
0 && (module.exports = {
|
|
634
946
|
TiquoAuth,
|
|
635
947
|
TiquoAuthError,
|
|
948
|
+
addCustomerUserId,
|
|
949
|
+
clearCachedEmail,
|
|
950
|
+
getCustomerUserIds,
|
|
951
|
+
getPrefilledEmailFromCookie,
|
|
952
|
+
isClerkAuthenticated,
|
|
953
|
+
trackCustomerPresence,
|
|
636
954
|
useTiquoAuth
|
|
637
955
|
});
|