@seaverse/auth-sdk 0.2.6 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1215,12 +1215,17 @@ class SeaVerseBackendAPIClient {
1215
1215
  'X-App-ID': this.appId,
1216
1216
  ...options.headers,
1217
1217
  };
1218
+ // 设置重试配置,默认禁用重试(maxRetries: 0)
1219
+ const defaultRetryOptions = {
1220
+ maxRetries: 0,
1221
+ };
1218
1222
  const httpOptions = {
1219
1223
  baseURL: finalBaseURL,
1220
1224
  timeout: options.timeout,
1221
1225
  headers,
1222
1226
  auth: options.auth || this.getDefaultAuth(),
1223
1227
  hooks: options.hooks || this.getDefaultHooks(),
1228
+ retryOptions: options.retryOptions || defaultRetryOptions,
1224
1229
  };
1225
1230
  this.httpClient = new HttpClient(httpOptions);
1226
1231
  }
@@ -1305,7 +1310,7 @@ class SeaVerseBackendAPIClient {
1305
1310
  */
1306
1311
  async register(data, options) {
1307
1312
  // 如果没有传 frontend_url,使用当前页面地址
1308
- const frontend_url = data.frontend_url || (typeof window !== 'undefined' ? window.location.origin : '');
1313
+ const frontend_url = data.frontend_url || (typeof window !== 'undefined' ? window.location.href : '');
1309
1314
  const config = {
1310
1315
  method: 'POST',
1311
1316
  url: `/sdk/v1/auth/register`,
@@ -1329,10 +1334,12 @@ class SeaVerseBackendAPIClient {
1329
1334
  * Login with email and password
1330
1335
  */
1331
1336
  async login(data, options) {
1337
+ // 如果没有传 frontend_url,使用当前页面地址
1338
+ const frontend_url = data.frontend_url || (typeof window !== 'undefined' ? window.location.href : '');
1332
1339
  const config = {
1333
1340
  method: 'POST',
1334
1341
  url: `/sdk/v1/auth/login`,
1335
- data,
1342
+ data: { ...data, frontend_url },
1336
1343
  headers: {
1337
1344
  'X-Operation-Id': 'login',
1338
1345
  ...options?.headers,
@@ -1396,7 +1403,7 @@ class SeaVerseBackendAPIClient {
1396
1403
  */
1397
1404
  async forgotPassword(data, options) {
1398
1405
  // 如果没有传 frontend_url,使用当前页面地址
1399
- const frontend_url = data.frontend_url || (typeof window !== 'undefined' ? window.location.origin : '');
1406
+ const frontend_url = data.frontend_url || (typeof window !== 'undefined' ? window.location.href : '');
1400
1407
  const config = {
1401
1408
  method: 'POST',
1402
1409
  url: `/sdk/v1/auth/forgot-password`,
@@ -1428,6 +1435,34 @@ class SeaVerseBackendAPIClient {
1428
1435
  const response = await this.httpClient.request(config);
1429
1436
  return response.data;
1430
1437
  }
1438
+ /**
1439
+ * Verify email with token
1440
+ * Verify user email and return JWT tokens for auto-login
1441
+ *
1442
+ * @param verifyToken - Email verification token from email link
1443
+ * @param options - Additional axios request options
1444
+ * @returns Email verification response with user data and JWT tokens
1445
+ *
1446
+ * @example
1447
+ * const { data } = await client.verifyEmail('abc123def456...');
1448
+ * localStorage.setItem('token', data.token);
1449
+ * localStorage.setItem('refreshToken', data.refreshToken);
1450
+ * console.log('User:', data.user);
1451
+ */
1452
+ async verifyEmail(verifyToken, options) {
1453
+ const config = {
1454
+ method: 'GET',
1455
+ url: `/sdk/v1/auth/email/verify`,
1456
+ params: { verify_token: verifyToken },
1457
+ headers: {
1458
+ 'X-Operation-Id': 'verifyEmail',
1459
+ ...options?.headers,
1460
+ },
1461
+ ...options,
1462
+ };
1463
+ const response = await this.httpClient.request(config);
1464
+ return response.data;
1465
+ }
1431
1466
  /**
1432
1467
  * Get api-service token
1433
1468
  * Generate token for accessing api-service from sandbox
@@ -1446,13 +1481,162 @@ class SeaVerseBackendAPIClient {
1446
1481
  return response.data;
1447
1482
  }
1448
1483
  // ============================================================================
1484
+ // Invite Code Management APIs
1485
+ // ============================================================================
1486
+ /**
1487
+ * List my invite codes
1488
+ * Get all invite codes created by the current user
1489
+ *
1490
+ * @param params - Optional pagination and filtering parameters
1491
+ * @param options - Additional axios request options
1492
+ * @returns List of invite codes with pagination info
1493
+ *
1494
+ * @example
1495
+ * // List all active invite codes
1496
+ * const result = await client.listInvites({
1497
+ * status: 'active',
1498
+ * page: 1,
1499
+ * page_size: 20
1500
+ * });
1501
+ * console.log('Invites:', result.data.invites);
1502
+ */
1503
+ async listInvites(params, options) {
1504
+ const config = {
1505
+ method: 'GET',
1506
+ url: `/sdk/v1/auth/invites`,
1507
+ params,
1508
+ headers: {
1509
+ 'X-Operation-Id': 'listInvites',
1510
+ ...options?.headers,
1511
+ },
1512
+ ...options,
1513
+ };
1514
+ const response = await this.httpClient.request(config);
1515
+ return response.data;
1516
+ }
1517
+ /**
1518
+ * Get invite code statistics
1519
+ * Get statistics for invite codes created by the current user
1520
+ *
1521
+ * @param options - Additional axios request options
1522
+ * @returns Invite code statistics
1523
+ *
1524
+ * @example
1525
+ * const result = await client.getInviteStats();
1526
+ * console.log('Total codes:', result.data.total_codes);
1527
+ * console.log('Total uses:', result.data.total_uses);
1528
+ */
1529
+ async getInviteStats(options) {
1530
+ const config = {
1531
+ method: 'GET',
1532
+ url: `/sdk/v1/auth/invites/stats`,
1533
+ headers: {
1534
+ 'X-Operation-Id': 'getInviteStats',
1535
+ ...options?.headers,
1536
+ },
1537
+ ...options,
1538
+ };
1539
+ const response = await this.httpClient.request(config);
1540
+ return response.data;
1541
+ }
1542
+ /**
1543
+ * Get invite code details
1544
+ * Get detailed information for a specific invite code
1545
+ *
1546
+ * @param inviteId - Invite code ID
1547
+ * @param options - Additional axios request options
1548
+ * @returns Invite code details
1549
+ *
1550
+ * @example
1551
+ * const result = await client.getInvite('inv_abc123');
1552
+ * console.log('Code:', result.data.code);
1553
+ * console.log('Used:', result.data.used_count);
1554
+ */
1555
+ async getInvite(inviteId, options) {
1556
+ const config = {
1557
+ method: 'GET',
1558
+ url: `/sdk/v1/auth/invites/${inviteId}`,
1559
+ headers: {
1560
+ 'X-Operation-Id': 'getInvite',
1561
+ ...options?.headers,
1562
+ },
1563
+ ...options,
1564
+ };
1565
+ const response = await this.httpClient.request(config);
1566
+ return response.data;
1567
+ }
1568
+ /**
1569
+ * Get invite code usage records
1570
+ * Get all usage records for a specific invite code
1571
+ *
1572
+ * @param inviteId - Invite code ID
1573
+ * @param params - Optional pagination parameters
1574
+ * @param options - Additional axios request options
1575
+ * @returns List of usage records with pagination info
1576
+ *
1577
+ * @example
1578
+ * const result = await client.getInviteUsages('inv_abc123', {
1579
+ * page: 1,
1580
+ * page_size: 20
1581
+ * });
1582
+ * console.log('Usages:', result.data.usages);
1583
+ */
1584
+ async getInviteUsages(inviteId, params, options) {
1585
+ const config = {
1586
+ method: 'GET',
1587
+ url: `/sdk/v1/auth/invites/${inviteId}/usages`,
1588
+ params,
1589
+ headers: {
1590
+ 'X-Operation-Id': 'getInviteUsages',
1591
+ ...options?.headers,
1592
+ },
1593
+ ...options,
1594
+ };
1595
+ const response = await this.httpClient.request(config);
1596
+ return response.data;
1597
+ }
1598
+ /**
1599
+ * Bind invitation code to temporary account
1600
+ * Activate a temporary account by binding an invitation code
1601
+ *
1602
+ * @param data - Bind invite code request
1603
+ * @param options - Additional axios request options
1604
+ * @returns Activation response with JWT tokens and user info
1605
+ *
1606
+ * @example
1607
+ * // Bind invitation code to activate temporary account
1608
+ * const result = await client.bindInviteCode({
1609
+ * user_id: 'user_temp_xyz789',
1610
+ * invite_code: 'ABCD1234'
1611
+ * });
1612
+ *
1613
+ * // Auto-login with returned tokens
1614
+ * localStorage.setItem('token', result.data.token);
1615
+ * localStorage.setItem('refreshToken', result.data.refreshToken);
1616
+ * console.log('Account activated:', result.data.user);
1617
+ */
1618
+ async bindInviteCode(data, options) {
1619
+ const config = {
1620
+ method: 'POST',
1621
+ url: `/sdk/v1/auth/bind-invite-code`,
1622
+ data,
1623
+ headers: {
1624
+ 'X-Operation-Id': 'bindInviteCode',
1625
+ ...options?.headers,
1626
+ },
1627
+ ...options,
1628
+ };
1629
+ const response = await this.httpClient.request(config);
1630
+ return response.data;
1631
+ }
1632
+ // ============================================================================
1449
1633
  // OAuth APIs
1450
1634
  // ============================================================================
1451
1635
  /**
1452
1636
  * Google OAuth authorization (Backend Proxy Mode)
1453
1637
  * Generate OAuth authorization URL for Google login
1454
1638
  *
1455
- * @param data - OAuth authorize request (return_url is optional, defaults to window.location.origin)
1639
+ * @param data - OAuth authorize request (return_url is optional, defaults to window.location.href)
1456
1640
  * @param options - Additional axios request options
1457
1641
  *
1458
1642
  * @example
@@ -1469,7 +1653,7 @@ class SeaVerseBackendAPIClient {
1469
1653
  */
1470
1654
  async googleAuthorize(data, options) {
1471
1655
  // 如果没有传 return_url,使用当前页面地址
1472
- const return_url = data?.return_url || (typeof window !== 'undefined' ? window.location.origin : '');
1656
+ const return_url = data?.return_url || (typeof window !== 'undefined' ? window.location.href : '');
1473
1657
  const config = {
1474
1658
  method: 'POST',
1475
1659
  url: `/sdk/v1/auth/google/authorize`,
@@ -1504,12 +1688,12 @@ class SeaVerseBackendAPIClient {
1504
1688
  * Discord OAuth authorization (Backend Proxy Mode)
1505
1689
  * Generate OAuth authorization URL for Discord login
1506
1690
  *
1507
- * @param data - OAuth authorize request (return_url is optional, defaults to window.location.origin)
1691
+ * @param data - OAuth authorize request (return_url is optional, defaults to window.location.href)
1508
1692
  * @param options - Additional axios request options
1509
1693
  */
1510
1694
  async discordAuthorize(data, options) {
1511
1695
  // 如果没有传 return_url,使用当前页面地址
1512
- const return_url = data?.return_url || (typeof window !== 'undefined' ? window.location.origin : '');
1696
+ const return_url = data?.return_url || (typeof window !== 'undefined' ? window.location.href : '');
1513
1697
  const config = {
1514
1698
  method: 'POST',
1515
1699
  url: `/sdk/v1/auth/discord/authorize`,
@@ -1543,12 +1727,12 @@ class SeaVerseBackendAPIClient {
1543
1727
  * GitHub OAuth authorization (Backend Proxy Mode)
1544
1728
  * Generate OAuth authorization URL for GitHub login
1545
1729
  *
1546
- * @param data - OAuth authorize request (return_url is optional, defaults to window.location.origin)
1730
+ * @param data - OAuth authorize request (return_url is optional, defaults to window.location.href)
1547
1731
  * @param options - Additional axios request options
1548
1732
  */
1549
1733
  async githubAuthorize(data, options) {
1550
1734
  // 如果没有传 return_url,使用当前页面地址
1551
- const return_url = data?.return_url || (typeof window !== 'undefined' ? window.location.origin : '');
1735
+ const return_url = data?.return_url || (typeof window !== 'undefined' ? window.location.href : '');
1552
1736
  const config = {
1553
1737
  method: 'POST',
1554
1738
  url: `/sdk/v1/auth/github/authorize`,
@@ -1833,15 +2017,559 @@ class SeaVerseBackendAPIClient {
1833
2017
  }
1834
2018
  }
1835
2019
 
2020
+ /**
2021
+ * Toast Notification System
2022
+ * A modern, glass-morphism inspired notification component
2023
+ */
2024
+ class Toast {
2025
+ /**
2026
+ * Show a toast notification
2027
+ */
2028
+ static show(options) {
2029
+ const { type, title, message, duration = 3000, onClose, } = options;
2030
+ // Ensure CSS is injected
2031
+ if (!this.cssInjected) {
2032
+ this.injectCSS();
2033
+ }
2034
+ // Ensure container exists
2035
+ if (!this.container) {
2036
+ this.createContainer();
2037
+ }
2038
+ // Create toast element
2039
+ const toast = this.createToast(type, title, message, onClose);
2040
+ // Add to container
2041
+ this.container.appendChild(toast);
2042
+ this.toasts.push(toast);
2043
+ // Trigger slide-in animation
2044
+ requestAnimationFrame(() => {
2045
+ toast.classList.add('toast-show');
2046
+ });
2047
+ // Auto-dismiss
2048
+ if (duration > 0) {
2049
+ setTimeout(() => {
2050
+ this.dismiss(toast, onClose);
2051
+ }, duration);
2052
+ }
2053
+ }
2054
+ /**
2055
+ * Convenience methods for different types
2056
+ */
2057
+ static success(title, message, duration) {
2058
+ this.show({ type: 'success', title, message, duration });
2059
+ }
2060
+ static error(title, message, duration) {
2061
+ this.show({ type: 'error', title, message, duration });
2062
+ }
2063
+ static warning(title, message, duration) {
2064
+ this.show({ type: 'warning', title, message, duration });
2065
+ }
2066
+ static info(title, message, duration) {
2067
+ this.show({ type: 'info', title, message, duration });
2068
+ }
2069
+ /**
2070
+ * Create the toast container
2071
+ */
2072
+ static createContainer() {
2073
+ this.container = document.createElement('div');
2074
+ this.container.className = 'toast-container';
2075
+ document.body.appendChild(this.container);
2076
+ }
2077
+ /**
2078
+ * Create a toast element using safe DOM methods
2079
+ */
2080
+ static createToast(type, title, message, onClose) {
2081
+ const id = `toast-${this.nextId++}`;
2082
+ const toast = document.createElement('div');
2083
+ toast.className = `toast toast-${type}`;
2084
+ toast.id = id;
2085
+ // Icon container
2086
+ const iconContainer = document.createElement('div');
2087
+ iconContainer.className = 'toast-icon';
2088
+ const iconSvg = this.createIconSVG(type);
2089
+ iconContainer.appendChild(iconSvg);
2090
+ toast.appendChild(iconContainer);
2091
+ // Content container
2092
+ const contentContainer = document.createElement('div');
2093
+ contentContainer.className = 'toast-content';
2094
+ const titleElement = document.createElement('div');
2095
+ titleElement.className = 'toast-title';
2096
+ titleElement.textContent = title;
2097
+ contentContainer.appendChild(titleElement);
2098
+ const messageElement = document.createElement('div');
2099
+ messageElement.className = 'toast-message';
2100
+ messageElement.textContent = message;
2101
+ contentContainer.appendChild(messageElement);
2102
+ toast.appendChild(contentContainer);
2103
+ // Close button
2104
+ const closeBtn = document.createElement('button');
2105
+ closeBtn.className = 'toast-close';
2106
+ closeBtn.setAttribute('aria-label', 'Close notification');
2107
+ closeBtn.addEventListener('click', () => {
2108
+ this.dismiss(toast, onClose);
2109
+ });
2110
+ // Close icon SVG
2111
+ const closeSvg = this.createCloseSVG();
2112
+ closeBtn.appendChild(closeSvg);
2113
+ toast.appendChild(closeBtn);
2114
+ return toast;
2115
+ }
2116
+ /**
2117
+ * Create icon SVG element for each type
2118
+ */
2119
+ static createIconSVG(type) {
2120
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
2121
+ svg.setAttribute('width', '24');
2122
+ svg.setAttribute('height', '24');
2123
+ svg.setAttribute('viewBox', '0 0 24 24');
2124
+ svg.setAttribute('fill', 'none');
2125
+ if (type === 'success') {
2126
+ // Background circle
2127
+ const bgCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
2128
+ bgCircle.setAttribute('cx', '12');
2129
+ bgCircle.setAttribute('cy', '12');
2130
+ bgCircle.setAttribute('r', '10');
2131
+ bgCircle.setAttribute('stroke', 'currentColor');
2132
+ bgCircle.setAttribute('stroke-width', '2');
2133
+ bgCircle.setAttribute('opacity', '0.2');
2134
+ svg.appendChild(bgCircle);
2135
+ // Animated circle
2136
+ const animCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
2137
+ animCircle.setAttribute('class', 'toast-icon-circle');
2138
+ animCircle.setAttribute('cx', '12');
2139
+ animCircle.setAttribute('cy', '12');
2140
+ animCircle.setAttribute('r', '10');
2141
+ animCircle.setAttribute('stroke', 'currentColor');
2142
+ animCircle.setAttribute('stroke-width', '2');
2143
+ animCircle.setAttribute('stroke-dasharray', '63');
2144
+ animCircle.setAttribute('stroke-dashoffset', '63');
2145
+ svg.appendChild(animCircle);
2146
+ // Check mark
2147
+ const check = document.createElementNS('http://www.w3.org/2000/svg', 'path');
2148
+ check.setAttribute('class', 'toast-icon-check');
2149
+ check.setAttribute('d', 'M8 12.5L10.5 15L16 9.5');
2150
+ check.setAttribute('stroke', 'currentColor');
2151
+ check.setAttribute('stroke-width', '2');
2152
+ check.setAttribute('stroke-linecap', 'round');
2153
+ check.setAttribute('stroke-linejoin', 'round');
2154
+ check.setAttribute('opacity', '0');
2155
+ svg.appendChild(check);
2156
+ }
2157
+ else if (type === 'error') {
2158
+ // Background circle
2159
+ const bgCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
2160
+ bgCircle.setAttribute('cx', '12');
2161
+ bgCircle.setAttribute('cy', '12');
2162
+ bgCircle.setAttribute('r', '10');
2163
+ bgCircle.setAttribute('stroke', 'currentColor');
2164
+ bgCircle.setAttribute('stroke-width', '2');
2165
+ bgCircle.setAttribute('opacity', '0.2');
2166
+ svg.appendChild(bgCircle);
2167
+ // Animated circle
2168
+ const animCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
2169
+ animCircle.setAttribute('class', 'toast-icon-circle');
2170
+ animCircle.setAttribute('cx', '12');
2171
+ animCircle.setAttribute('cy', '12');
2172
+ animCircle.setAttribute('r', '10');
2173
+ animCircle.setAttribute('stroke', 'currentColor');
2174
+ animCircle.setAttribute('stroke-width', '2');
2175
+ animCircle.setAttribute('stroke-dasharray', '63');
2176
+ animCircle.setAttribute('stroke-dashoffset', '63');
2177
+ svg.appendChild(animCircle);
2178
+ // X mark
2179
+ const xMark = document.createElementNS('http://www.w3.org/2000/svg', 'path');
2180
+ xMark.setAttribute('class', 'toast-icon-x');
2181
+ xMark.setAttribute('d', 'M9 9L15 15M15 9L9 15');
2182
+ xMark.setAttribute('stroke', 'currentColor');
2183
+ xMark.setAttribute('stroke-width', '2');
2184
+ xMark.setAttribute('stroke-linecap', 'round');
2185
+ xMark.setAttribute('opacity', '0');
2186
+ svg.appendChild(xMark);
2187
+ }
2188
+ else if (type === 'warning') {
2189
+ // Background triangle
2190
+ const bgTriangle = document.createElementNS('http://www.w3.org/2000/svg', 'path');
2191
+ bgTriangle.setAttribute('d', 'M12 2L2 20H22L12 2Z');
2192
+ bgTriangle.setAttribute('stroke', 'currentColor');
2193
+ bgTriangle.setAttribute('stroke-width', '2');
2194
+ bgTriangle.setAttribute('opacity', '0.2');
2195
+ svg.appendChild(bgTriangle);
2196
+ // Animated triangle
2197
+ const animTriangle = document.createElementNS('http://www.w3.org/2000/svg', 'path');
2198
+ animTriangle.setAttribute('class', 'toast-icon-triangle');
2199
+ animTriangle.setAttribute('d', 'M12 2L2 20H22L12 2Z');
2200
+ animTriangle.setAttribute('stroke', 'currentColor');
2201
+ animTriangle.setAttribute('stroke-width', '2');
2202
+ animTriangle.setAttribute('stroke-dasharray', '80');
2203
+ animTriangle.setAttribute('stroke-dashoffset', '80');
2204
+ svg.appendChild(animTriangle);
2205
+ // Exclamation mark
2206
+ const exclaim = document.createElementNS('http://www.w3.org/2000/svg', 'path');
2207
+ exclaim.setAttribute('class', 'toast-icon-exclaim');
2208
+ exclaim.setAttribute('d', 'M12 9V13M12 16V16.5');
2209
+ exclaim.setAttribute('stroke', 'currentColor');
2210
+ exclaim.setAttribute('stroke-width', '2');
2211
+ exclaim.setAttribute('stroke-linecap', 'round');
2212
+ exclaim.setAttribute('opacity', '0');
2213
+ svg.appendChild(exclaim);
2214
+ }
2215
+ else { // info
2216
+ // Background circle
2217
+ const bgCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
2218
+ bgCircle.setAttribute('cx', '12');
2219
+ bgCircle.setAttribute('cy', '12');
2220
+ bgCircle.setAttribute('r', '10');
2221
+ bgCircle.setAttribute('stroke', 'currentColor');
2222
+ bgCircle.setAttribute('stroke-width', '2');
2223
+ bgCircle.setAttribute('opacity', '0.2');
2224
+ svg.appendChild(bgCircle);
2225
+ // Animated circle
2226
+ const animCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
2227
+ animCircle.setAttribute('class', 'toast-icon-circle');
2228
+ animCircle.setAttribute('cx', '12');
2229
+ animCircle.setAttribute('cy', '12');
2230
+ animCircle.setAttribute('r', '10');
2231
+ animCircle.setAttribute('stroke', 'currentColor');
2232
+ animCircle.setAttribute('stroke-width', '2');
2233
+ animCircle.setAttribute('stroke-dasharray', '63');
2234
+ animCircle.setAttribute('stroke-dashoffset', '63');
2235
+ svg.appendChild(animCircle);
2236
+ // Info i
2237
+ const iMark = document.createElementNS('http://www.w3.org/2000/svg', 'path');
2238
+ iMark.setAttribute('class', 'toast-icon-i');
2239
+ iMark.setAttribute('d', 'M12 8V8.5M12 12V16');
2240
+ iMark.setAttribute('stroke', 'currentColor');
2241
+ iMark.setAttribute('stroke-width', '2');
2242
+ iMark.setAttribute('stroke-linecap', 'round');
2243
+ iMark.setAttribute('opacity', '0');
2244
+ svg.appendChild(iMark);
2245
+ }
2246
+ return svg;
2247
+ }
2248
+ /**
2249
+ * Create close button SVG
2250
+ */
2251
+ static createCloseSVG() {
2252
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
2253
+ svg.setAttribute('width', '16');
2254
+ svg.setAttribute('height', '16');
2255
+ svg.setAttribute('viewBox', '0 0 16 16');
2256
+ svg.setAttribute('fill', 'none');
2257
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
2258
+ path.setAttribute('d', 'M12 4L4 12M4 4L12 12');
2259
+ path.setAttribute('stroke', 'currentColor');
2260
+ path.setAttribute('stroke-width', '1.5');
2261
+ path.setAttribute('stroke-linecap', 'round');
2262
+ svg.appendChild(path);
2263
+ return svg;
2264
+ }
2265
+ /**
2266
+ * Dismiss a toast
2267
+ */
2268
+ static dismiss(toast, onClose) {
2269
+ // Trigger slide-out animation
2270
+ toast.classList.add('toast-hide');
2271
+ // Remove from DOM after animation
2272
+ setTimeout(() => {
2273
+ if (toast.parentNode) {
2274
+ toast.parentNode.removeChild(toast);
2275
+ }
2276
+ // Remove from array
2277
+ const index = this.toasts.indexOf(toast);
2278
+ if (index > -1) {
2279
+ this.toasts.splice(index, 1);
2280
+ }
2281
+ // Call onClose callback
2282
+ if (onClose) {
2283
+ onClose();
2284
+ }
2285
+ // Remove container if no toasts left
2286
+ if (this.toasts.length === 0 && this.container) {
2287
+ this.container.remove();
2288
+ this.container = null;
2289
+ }
2290
+ }, 300);
2291
+ }
2292
+ /**
2293
+ * Dismiss all toasts
2294
+ */
2295
+ static dismissAll() {
2296
+ this.toasts.forEach(toast => {
2297
+ this.dismiss(toast);
2298
+ });
2299
+ }
2300
+ /**
2301
+ * Inject Toast CSS into the page
2302
+ */
2303
+ static injectCSS() {
2304
+ if (this.cssInjected)
2305
+ return;
2306
+ const cssContent = `
2307
+ /* Toast Container */
2308
+ .toast-container {
2309
+ position: fixed;
2310
+ top: 24px;
2311
+ right: 24px;
2312
+ z-index: 10000;
2313
+ display: flex;
2314
+ flex-direction: column;
2315
+ gap: 12px;
2316
+ pointer-events: none;
2317
+ max-width: 420px;
2318
+ width: calc(100vw - 48px);
2319
+ }
2320
+
2321
+ @media (max-width: 640px) {
2322
+ .toast-container {
2323
+ top: 16px;
2324
+ right: 16px;
2325
+ left: 16px;
2326
+ width: auto;
2327
+ max-width: none;
2328
+ }
2329
+ }
2330
+
2331
+ /* Toast Base */
2332
+ .toast {
2333
+ position: relative;
2334
+ display: flex;
2335
+ align-items: flex-start;
2336
+ gap: 14px;
2337
+ padding: 18px 20px;
2338
+ border-radius: 16px;
2339
+ background: rgba(20, 20, 24, 0.92);
2340
+ backdrop-filter: blur(20px) saturate(180%);
2341
+ -webkit-backdrop-filter: blur(20px) saturate(180%);
2342
+ border: 1px solid rgba(255, 255, 255, 0.08);
2343
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4), 0 8px 32px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.05);
2344
+ pointer-events: auto;
2345
+ transform: translateY(-120%) translateZ(0);
2346
+ opacity: 0;
2347
+ transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
2348
+ will-change: transform, opacity;
2349
+ }
2350
+
2351
+ .toast-show {
2352
+ transform: translateY(0) translateZ(0);
2353
+ opacity: 1;
2354
+ }
2355
+
2356
+ .toast-hide {
2357
+ transform: translateY(-120%) translateZ(0);
2358
+ opacity: 0;
2359
+ transition: all 0.25s cubic-bezier(0.4, 0, 1, 1);
2360
+ }
2361
+
2362
+ .toast-icon {
2363
+ flex-shrink: 0;
2364
+ width: 24px;
2365
+ height: 24px;
2366
+ margin-top: 2px;
2367
+ position: relative;
2368
+ }
2369
+
2370
+ @keyframes drawCircle {
2371
+ to { stroke-dashoffset: 0; }
2372
+ }
2373
+
2374
+ @keyframes drawTriangle {
2375
+ to { stroke-dashoffset: 0; }
2376
+ }
2377
+
2378
+ @keyframes fadeInIcon {
2379
+ to { opacity: 1; }
2380
+ }
2381
+
2382
+ .toast-show .toast-icon-circle {
2383
+ animation: drawCircle 0.5s cubic-bezier(0.4, 0, 0.2, 1) 0.1s forwards;
2384
+ }
2385
+
2386
+ .toast-show .toast-icon-triangle {
2387
+ animation: drawTriangle 0.5s cubic-bezier(0.4, 0, 0.2, 1) 0.1s forwards;
2388
+ }
2389
+
2390
+ .toast-show .toast-icon-check,
2391
+ .toast-show .toast-icon-x,
2392
+ .toast-show .toast-icon-exclaim,
2393
+ .toast-show .toast-icon-i {
2394
+ animation: fadeInIcon 0.3s ease-out 0.4s forwards;
2395
+ }
2396
+
2397
+ .toast-content {
2398
+ flex: 1;
2399
+ min-width: 0;
2400
+ padding-top: 1px;
2401
+ }
2402
+
2403
+ .toast-title {
2404
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
2405
+ font-size: 15px;
2406
+ font-weight: 700;
2407
+ line-height: 1.4;
2408
+ letter-spacing: -0.01em;
2409
+ color: rgba(255, 255, 255, 0.95);
2410
+ margin-bottom: 4px;
2411
+ }
2412
+
2413
+ .toast-message {
2414
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
2415
+ font-size: 13.5px;
2416
+ font-weight: 400;
2417
+ line-height: 1.5;
2418
+ letter-spacing: -0.005em;
2419
+ color: rgba(255, 255, 255, 0.65);
2420
+ }
2421
+
2422
+ .toast-close {
2423
+ flex-shrink: 0;
2424
+ width: 28px;
2425
+ height: 28px;
2426
+ display: flex;
2427
+ align-items: center;
2428
+ justify-content: center;
2429
+ border: none;
2430
+ background: transparent;
2431
+ color: rgba(255, 255, 255, 0.4);
2432
+ cursor: pointer;
2433
+ border-radius: 8px;
2434
+ transition: all 0.2s ease;
2435
+ margin: -4px -6px 0 0;
2436
+ }
2437
+
2438
+ .toast-close:hover {
2439
+ background: rgba(255, 255, 255, 0.08);
2440
+ color: rgba(255, 255, 255, 0.7);
2441
+ }
2442
+
2443
+ .toast-close:active {
2444
+ transform: scale(0.92);
2445
+ }
2446
+
2447
+ .toast-success {
2448
+ border-color: rgba(52, 211, 153, 0.2);
2449
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4), 0 8px 32px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.05), 0 0 0 1px rgba(52, 211, 153, 0.15), 0 0 24px rgba(52, 211, 153, 0.12);
2450
+ }
2451
+
2452
+ .toast-success .toast-icon {
2453
+ color: #34d399;
2454
+ filter: drop-shadow(0 0 8px rgba(52, 211, 153, 0.4));
2455
+ }
2456
+
2457
+ @keyframes successGlow {
2458
+ 0%, 100% { box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4), 0 8px 32px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.05), 0 0 0 1px rgba(52, 211, 153, 0.15), 0 0 24px rgba(52, 211, 153, 0.12); }
2459
+ 50% { box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4), 0 8px 32px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.05), 0 0 0 1px rgba(52, 211, 153, 0.25), 0 0 32px rgba(52, 211, 153, 0.2); }
2460
+ }
2461
+
2462
+ .toast-success.toast-show {
2463
+ animation: successGlow 2s ease-in-out infinite;
2464
+ }
2465
+
2466
+ .toast-error {
2467
+ border-color: rgba(248, 113, 113, 0.2);
2468
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4), 0 8px 32px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.05), 0 0 0 1px rgba(248, 113, 113, 0.15), 0 0 24px rgba(248, 113, 113, 0.12);
2469
+ }
2470
+
2471
+ .toast-error .toast-icon {
2472
+ color: #f87171;
2473
+ filter: drop-shadow(0 0 8px rgba(248, 113, 113, 0.4));
2474
+ }
2475
+
2476
+ @keyframes errorGlow {
2477
+ 0%, 100% { box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4), 0 8px 32px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.05), 0 0 0 1px rgba(248, 113, 113, 0.15), 0 0 24px rgba(248, 113, 113, 0.12); }
2478
+ 50% { box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4), 0 8px 32px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.05), 0 0 0 1px rgba(248, 113, 113, 0.25), 0 0 32px rgba(248, 113, 113, 0.2); }
2479
+ }
2480
+
2481
+ .toast-error.toast-show {
2482
+ animation: errorGlow 2s ease-in-out infinite;
2483
+ }
2484
+
2485
+ .toast-warning {
2486
+ border-color: rgba(251, 191, 36, 0.2);
2487
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4), 0 8px 32px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.05), 0 0 0 1px rgba(251, 191, 36, 0.15), 0 0 24px rgba(251, 191, 36, 0.12);
2488
+ }
2489
+
2490
+ .toast-warning .toast-icon {
2491
+ color: #fbbf24;
2492
+ filter: drop-shadow(0 0 8px rgba(251, 191, 36, 0.4));
2493
+ }
2494
+
2495
+ @keyframes warningGlow {
2496
+ 0%, 100% { box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4), 0 8px 32px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.05), 0 0 0 1px rgba(251, 191, 36, 0.15), 0 0 24px rgba(251, 191, 36, 0.12); }
2497
+ 50% { box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4), 0 8px 32px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.05), 0 0 0 1px rgba(251, 191, 36, 0.25), 0 0 32px rgba(251, 191, 36, 0.2); }
2498
+ }
2499
+
2500
+ .toast-warning.toast-show {
2501
+ animation: warningGlow 2s ease-in-out infinite;
2502
+ }
2503
+
2504
+ .toast-info {
2505
+ border-color: rgba(96, 165, 250, 0.2);
2506
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4), 0 8px 32px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.05), 0 0 0 1px rgba(96, 165, 250, 0.15), 0 0 24px rgba(96, 165, 250, 0.12);
2507
+ }
2508
+
2509
+ .toast-info .toast-icon {
2510
+ color: #60a5fa;
2511
+ filter: drop-shadow(0 0 8px rgba(96, 165, 250, 0.4));
2512
+ }
2513
+
2514
+ @keyframes infoGlow {
2515
+ 0%, 100% { box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4), 0 8px 32px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.05), 0 0 0 1px rgba(96, 165, 250, 0.15), 0 0 24px rgba(96, 165, 250, 0.12); }
2516
+ 50% { box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4), 0 8px 32px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.05), 0 0 0 1px rgba(96, 165, 250, 0.25), 0 0 32px rgba(96, 165, 250, 0.2); }
2517
+ }
2518
+
2519
+ .toast-info.toast-show {
2520
+ animation: infoGlow 2s ease-in-out infinite;
2521
+ }
2522
+
2523
+ @media (prefers-reduced-motion: reduce) {
2524
+ .toast {
2525
+ transition: opacity 0.2s ease;
2526
+ animation: none !important;
2527
+ }
2528
+ .toast-show {
2529
+ transform: none;
2530
+ }
2531
+ .toast-hide {
2532
+ transform: none;
2533
+ }
2534
+ .toast-icon-circle,
2535
+ .toast-icon-triangle,
2536
+ .toast-icon-check,
2537
+ .toast-icon-x,
2538
+ .toast-icon-exclaim,
2539
+ .toast-icon-i {
2540
+ animation: none !important;
2541
+ opacity: 1 !important;
2542
+ stroke-dashoffset: 0 !important;
2543
+ }
2544
+ }
2545
+ `;
2546
+ const style = document.createElement('style');
2547
+ style.id = 'toast-styles';
2548
+ style.textContent = cssContent;
2549
+ document.head.appendChild(style);
2550
+ this.cssInjected = true;
2551
+ }
2552
+ }
2553
+ Toast.container = null;
2554
+ Toast.toasts = [];
2555
+ Toast.nextId = 0;
2556
+ Toast.cssInjected = false;
2557
+
1836
2558
  class AuthModal {
1837
2559
  constructor(options) {
1838
2560
  this.modal = null;
1839
2561
  this.currentView = 'login';
1840
2562
  this.resetToken = null;
2563
+ this.tempUserId = null; // 临时用户ID(需要激活)
2564
+ this.tempUserEmail = null; // 临时用户邮箱
1841
2565
  this.client = options.client;
1842
2566
  this.options = options;
2567
+ // Auto-detect email verification token in URL
2568
+ this.checkForEmailVerification();
1843
2569
  // Auto-detect reset token in URL
1844
2570
  this.checkForResetToken();
2571
+ // Auto-detect invite code required error
2572
+ this.checkForInviteCodeRequired();
1845
2573
  }
1846
2574
  /**
1847
2575
  * Show the authentication modal
@@ -1952,6 +2680,9 @@ class AuthModal {
1952
2680
  // Reset password form
1953
2681
  const resetForm = this.createResetPasswordForm();
1954
2682
  rightPanel.appendChild(resetForm);
2683
+ // Invite code form
2684
+ const inviteCodeForm = this.createInviteCodeForm();
2685
+ rightPanel.appendChild(inviteCodeForm);
1955
2686
  // Success message
1956
2687
  const successMessage = this.createSuccessMessage();
1957
2688
  rightPanel.appendChild(successMessage);
@@ -2285,6 +3016,60 @@ class AuthModal {
2285
3016
  container.appendChild(footer);
2286
3017
  return container;
2287
3018
  }
3019
+ createInviteCodeForm() {
3020
+ const container = document.createElement('div');
3021
+ container.id = 'inviteCodeForm';
3022
+ container.className = 'auth-form-view hidden';
3023
+ // Icon
3024
+ const icon = document.createElement('div');
3025
+ icon.className = 'forgot-password-icon';
3026
+ icon.innerHTML = '<div class="icon-glow"></div><svg class="icon-lock" width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="11" width="14" height="10" rx="2" /><path d="M12 11V7a3 3 0 0 1 3-3h0a3 3 0 0 1 3 3v4" /><circle cx="12" cy="15" r="1" /></svg>';
3027
+ container.appendChild(icon);
3028
+ // Header
3029
+ const header = document.createElement('div');
3030
+ header.className = 'auth-form-header';
3031
+ const title = document.createElement('h2');
3032
+ title.className = 'auth-form-title';
3033
+ title.textContent = 'Activation Required';
3034
+ const subtitle = document.createElement('p');
3035
+ subtitle.className = 'auth-form-subtitle';
3036
+ subtitle.id = 'inviteCodeSubtitle';
3037
+ subtitle.textContent = 'Enter your invitation code to activate your account';
3038
+ header.appendChild(title);
3039
+ header.appendChild(subtitle);
3040
+ container.appendChild(header);
3041
+ // Form
3042
+ const form = document.createElement('form');
3043
+ form.id = 'inviteCodeFormElement';
3044
+ form.className = 'auth-form';
3045
+ const codeGroup = this.createFormGroup('inviteCodeInput', 'Invitation Code', 'text', 'Enter invitation code');
3046
+ form.appendChild(codeGroup);
3047
+ const submitBtn = document.createElement('button');
3048
+ submitBtn.type = 'submit';
3049
+ submitBtn.id = 'bindInviteCodeButton';
3050
+ submitBtn.className = 'btn-auth-primary';
3051
+ const btnText = document.createElement('span');
3052
+ btnText.className = 'btn-text';
3053
+ btnText.textContent = 'Activate Account';
3054
+ const btnLoader = document.createElement('span');
3055
+ btnLoader.className = 'btn-loader hidden';
3056
+ btnLoader.innerHTML = '<svg class="spinner" viewBox="0 0 24 24"><circle class="spinner-track" cx="12" cy="12" r="10"></circle><circle class="spinner-circle" cx="12" cy="12" r="10"></circle></svg>';
3057
+ submitBtn.appendChild(btnText);
3058
+ submitBtn.appendChild(btnLoader);
3059
+ form.appendChild(submitBtn);
3060
+ container.appendChild(form);
3061
+ // Footer
3062
+ const footer = document.createElement('div');
3063
+ footer.className = 'auth-footer forgot-footer';
3064
+ const backLink = document.createElement('a');
3065
+ backLink.href = '#';
3066
+ backLink.id = 'backToLoginFromInvite';
3067
+ backLink.className = 'back-to-login';
3068
+ backLink.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5M12 19l-7-7 7-7"/></svg><span>Back to Sign In</span>';
3069
+ footer.appendChild(backLink);
3070
+ container.appendChild(footer);
3071
+ return container;
3072
+ }
2288
3073
  createSuccessMessage() {
2289
3074
  const container = document.createElement('div');
2290
3075
  container.id = 'authMessage';
@@ -2374,12 +3159,59 @@ class AuthModal {
2374
3159
  button.appendChild(span);
2375
3160
  return button;
2376
3161
  }
3162
+ /**
3163
+ * Check if URL contains email verification token and auto-verify
3164
+ */
3165
+ async checkForEmailVerification() {
3166
+ const urlParams = new URLSearchParams(window.location.search);
3167
+ const verifyToken = urlParams.get('verify_token');
3168
+ if (!verifyToken) {
3169
+ return;
3170
+ }
3171
+ console.log('[AuthModal] Detected verify_token, starting email verification...');
3172
+ try {
3173
+ // Call email verification API
3174
+ const result = await this.client.verifyEmail(verifyToken);
3175
+ if (result.success && result.data.token) {
3176
+ console.log('[AuthModal] Email verification successful:', result.data.user);
3177
+ // Auto-login: set token in client
3178
+ this.client.setToken(result.data.token);
3179
+ // Clean up URL parameters
3180
+ urlParams.delete('verify_token');
3181
+ const newUrl = window.location.pathname + (urlParams.toString() ? '?' + urlParams.toString() : '');
3182
+ window.history.replaceState({}, '', newUrl);
3183
+ // Trigger onLoginSuccess callback
3184
+ if (this.options.onLoginSuccess) {
3185
+ this.options.onLoginSuccess(result.data.token, result.data.user);
3186
+ }
3187
+ // Show success message
3188
+ this.showSuccess('Email Verified!', 'Your email has been verified successfully. You are now logged in.');
3189
+ }
3190
+ else {
3191
+ throw new Error(result.data?.message || 'Email verification failed');
3192
+ }
3193
+ }
3194
+ catch (error) {
3195
+ console.error('[AuthModal] Email verification failed:', error);
3196
+ // Clean up URL on error too
3197
+ urlParams.delete('verify_token');
3198
+ const newUrl = window.location.pathname + (urlParams.toString() ? '?' + urlParams.toString() : '');
3199
+ window.history.replaceState({}, '', newUrl);
3200
+ // Show error message
3201
+ const errorMessage = error.response?.data?.error || error.message || 'Email verification failed';
3202
+ this.showError('Verification Failed', errorMessage + '. Please try registering again or contact support.');
3203
+ // Trigger error callback
3204
+ if (this.options.onError) {
3205
+ this.options.onError(error);
3206
+ }
3207
+ }
3208
+ }
2377
3209
  /**
2378
3210
  * Check if URL contains reset token and auto-show reset password form
2379
3211
  */
2380
3212
  checkForResetToken() {
2381
3213
  const urlParams = new URLSearchParams(window.location.search);
2382
- const verifyToken = urlParams.get('verify_token');
3214
+ const verifyToken = urlParams.get('reset_token');
2383
3215
  if (verifyToken) {
2384
3216
  this.resetToken = verifyToken;
2385
3217
  // Auto-show modal with reset password form
@@ -2388,6 +3220,58 @@ class AuthModal {
2388
3220
  }, 100);
2389
3221
  }
2390
3222
  }
3223
+ /**
3224
+ * Check if URL contains invite code required error and auto-show invite code form
3225
+ */
3226
+ checkForInviteCodeRequired() {
3227
+ const urlParams = new URLSearchParams(window.location.search);
3228
+ const errorCode = urlParams.get('error_code');
3229
+ const userId = urlParams.get('user_id');
3230
+ const tempUserId = urlParams.get('temp_user_id'); // Support alternative parameter name
3231
+ const email = urlParams.get('email');
3232
+ // Log for debugging
3233
+ if (errorCode === 'INVITE_CODE_REQUIRED') {
3234
+ console.log('[AuthModal] Detected INVITE_CODE_REQUIRED:', {
3235
+ errorCode,
3236
+ userId,
3237
+ tempUserId,
3238
+ email,
3239
+ fullURL: window.location.href
3240
+ });
3241
+ }
3242
+ if (errorCode === 'INVITE_CODE_REQUIRED') {
3243
+ // Use user_id or temp_user_id
3244
+ const finalUserId = userId || tempUserId;
3245
+ if (!finalUserId) {
3246
+ // Missing user_id - show error
3247
+ console.error('[AuthModal] Missing user_id in URL parameters. Backend should include user_id when redirecting with INVITE_CODE_REQUIRED.');
3248
+ alert('错误:后端重定向缺少 user_id 参数。请联系技术支持。\n\n完整URL: ' + window.location.href);
3249
+ // Clean up URL anyway
3250
+ const url = new URL(window.location.href);
3251
+ url.searchParams.delete('error_code');
3252
+ window.history.replaceState({}, document.title, url.toString());
3253
+ return;
3254
+ }
3255
+ this.tempUserId = finalUserId;
3256
+ this.tempUserEmail = email || '';
3257
+ // Clean up URL
3258
+ const url = new URL(window.location.href);
3259
+ url.searchParams.delete('error_code');
3260
+ url.searchParams.delete('user_id');
3261
+ url.searchParams.delete('temp_user_id');
3262
+ url.searchParams.delete('email');
3263
+ window.history.replaceState({}, document.title, url.toString());
3264
+ // Trigger callback if provided
3265
+ if (this.options.onInviteCodeRequired) {
3266
+ this.options.onInviteCodeRequired(finalUserId, email || '');
3267
+ }
3268
+ // Auto-show modal with invite code form
3269
+ console.log('[AuthModal] Auto-showing invite code form');
3270
+ setTimeout(() => {
3271
+ this.show('invite-code');
3272
+ }, 100);
3273
+ }
3274
+ }
2391
3275
  bindEventListeners() {
2392
3276
  if (!this.modal)
2393
3277
  return;
@@ -2423,6 +3307,11 @@ class AuthModal {
2423
3307
  e.preventDefault();
2424
3308
  this.switchView('login');
2425
3309
  });
3310
+ const backToLoginFromInvite = this.modal.querySelector('#backToLoginFromInvite');
3311
+ backToLoginFromInvite?.addEventListener('click', (e) => {
3312
+ e.preventDefault();
3313
+ this.switchView('login');
3314
+ });
2426
3315
  // Password toggle
2427
3316
  const passwordToggles = this.modal.querySelectorAll('.toggle-password');
2428
3317
  passwordToggles.forEach((toggle) => {
@@ -2455,6 +3344,8 @@ class AuthModal {
2455
3344
  forgotForm?.addEventListener('submit', (e) => this.handleForgotPassword(e));
2456
3345
  const resetPasswordForm = this.modal.querySelector('#resetPasswordFormElement');
2457
3346
  resetPasswordForm?.addEventListener('submit', (e) => this.handleResetPassword(e));
3347
+ const inviteCodeForm = this.modal.querySelector('#inviteCodeFormElement');
3348
+ inviteCodeForm?.addEventListener('submit', (e) => this.handleBindInviteCode(e));
2458
3349
  // OAuth social login buttons
2459
3350
  this.bindSocialLoginButtons();
2460
3351
  }
@@ -2501,7 +3392,7 @@ class AuthModal {
2501
3392
  switchView(view) {
2502
3393
  if (!this.modal)
2503
3394
  return;
2504
- const views = ['loginForm', 'signupForm', 'forgotPasswordForm', 'resetPasswordForm', 'authMessage'];
3395
+ const views = ['loginForm', 'signupForm', 'forgotPasswordForm', 'resetPasswordForm', 'inviteCodeForm', 'authMessage'];
2505
3396
  views.forEach((viewId) => {
2506
3397
  const element = this.modal.querySelector(`#${viewId}`);
2507
3398
  element?.classList.add('hidden');
@@ -2511,6 +3402,7 @@ class AuthModal {
2511
3402
  signup: 'signupForm',
2512
3403
  forgot: 'forgotPasswordForm',
2513
3404
  'reset-password': 'resetPasswordForm',
3405
+ 'invite-code': 'inviteCodeForm',
2514
3406
  message: 'authMessage',
2515
3407
  };
2516
3408
  const targetView = this.modal.querySelector(`#${viewMap[view]}`);
@@ -2543,7 +3435,7 @@ class AuthModal {
2543
3435
  if (this.options.onLoginSuccess) {
2544
3436
  this.options.onLoginSuccess(response.token, response.user);
2545
3437
  }
2546
- this.showMessage('Login Successful', 'Welcome back!');
3438
+ this.showSuccess('Login Successful', 'Welcome back!');
2547
3439
  }
2548
3440
  else {
2549
3441
  throw new Error('Invalid response from server');
@@ -2552,7 +3444,7 @@ class AuthModal {
2552
3444
  catch (error) {
2553
3445
  // Handle error
2554
3446
  const errorMessage = error instanceof Error ? error.message : 'Login failed';
2555
- this.showError(errorMessage);
3447
+ this.showError('Login Failed', errorMessage);
2556
3448
  if (this.options.onError) {
2557
3449
  this.options.onError(error);
2558
3450
  }
@@ -2579,7 +3471,7 @@ class AuthModal {
2579
3471
  const passwordConfirm = passwordConfirmInput.value;
2580
3472
  // Validate passwords match
2581
3473
  if (password !== passwordConfirm) {
2582
- this.showError('Passwords do not match');
3474
+ this.showError('Password Mismatch', 'Passwords do not match');
2583
3475
  return;
2584
3476
  }
2585
3477
  try {
@@ -2594,18 +3486,31 @@ class AuthModal {
2594
3486
  });
2595
3487
  // Handle success - Note: register returns different response format
2596
3488
  if (response.success) {
2597
- // After successful registration, perform login to get token
3489
+ // Check if email verification is required
3490
+ if (response.requiresEmailVerification) {
3491
+ this.showInfo('Email Verification Required', 'Please check your email and click the verification link to activate your account.');
3492
+ return; // Don't auto-login, user needs to verify email first
3493
+ }
3494
+ // Check if invitation code is required
3495
+ if (response.requiresInvitationCode && response.tempUserId) {
3496
+ this.tempUserId = response.tempUserId;
3497
+ this.tempUserEmail = email;
3498
+ this.showInfo('Activation Required', 'Please enter your invitation code to activate your account.');
3499
+ this.show('invite-code'); // Show invite code activation form
3500
+ return; // Don't auto-login, user needs to activate with invite code first
3501
+ }
3502
+ // Only auto-login if no verification or activation is needed
2598
3503
  const loginResponse = await this.client.login({ email, password });
2599
3504
  if (loginResponse.token) {
2600
3505
  if (this.options.onSignupSuccess) {
2601
3506
  this.options.onSignupSuccess(loginResponse.token, loginResponse.user);
2602
3507
  }
2603
- this.showMessage('Account Created', response.message || 'Your account has been created successfully!');
3508
+ this.showSuccess('Account Created', response.message || 'Your account has been created successfully!');
2604
3509
  }
2605
3510
  }
2606
3511
  else if (response.code === exports.ErrorCode.ACCOUNT_EXISTS) {
2607
3512
  // Handle account already exists error
2608
- this.showMessage('Account Already Exists', 'This email is already registered. Please login instead.');
3513
+ this.showWarning('Account Already Exists', 'This email is already registered. Please login instead.');
2609
3514
  }
2610
3515
  else {
2611
3516
  throw new Error(response.error || 'Registration failed');
@@ -2614,12 +3519,12 @@ class AuthModal {
2614
3519
  catch (error) {
2615
3520
  // Handle HTTP errors
2616
3521
  if (error.response?.data?.code === exports.ErrorCode.ACCOUNT_EXISTS) {
2617
- this.showMessage('Account Already Exists', 'This email is already registered. Please login instead.');
3522
+ this.showWarning('Account Already Exists', 'This email is already registered. Please login instead.');
2618
3523
  return;
2619
3524
  }
2620
3525
  // Handle other errors
2621
3526
  const errorMessage = error.response?.data?.error || error.message || 'Signup failed';
2622
- this.showError(errorMessage);
3527
+ this.showError('Registration Failed', errorMessage);
2623
3528
  if (this.options.onError) {
2624
3529
  this.options.onError(error);
2625
3530
  }
@@ -2644,12 +3549,12 @@ class AuthModal {
2644
3549
  // Call forgot password API
2645
3550
  await this.client.forgotPassword({ email });
2646
3551
  // Show success message
2647
- this.showMessage('Reset Link Sent', `We've sent a password reset link to ${email}`);
3552
+ this.showSuccess('Reset Link Sent', `We've sent a password reset link to ${email}`);
2648
3553
  }
2649
3554
  catch (error) {
2650
3555
  // Handle error
2651
3556
  const errorMessage = error instanceof Error ? error.message : 'Failed to send reset link';
2652
- this.showError(errorMessage);
3557
+ this.showError('Failed to Send Link', errorMessage);
2653
3558
  if (this.options.onError) {
2654
3559
  this.options.onError(error);
2655
3560
  }
@@ -2672,12 +3577,12 @@ class AuthModal {
2672
3577
  const confirmPassword = passwordConfirmInput.value;
2673
3578
  // Validate passwords match
2674
3579
  if (newPassword !== confirmPassword) {
2675
- this.showError('Passwords do not match');
3580
+ this.showError('Password Mismatch', 'Passwords do not match');
2676
3581
  return;
2677
3582
  }
2678
3583
  // Validate token exists
2679
3584
  if (!this.resetToken) {
2680
- this.showError('Reset token is missing. Please use the link from your email.');
3585
+ this.showError('Invalid Token', 'Reset token is missing. Please use the link from your email.');
2681
3586
  return;
2682
3587
  }
2683
3588
  try {
@@ -2690,19 +3595,19 @@ class AuthModal {
2690
3595
  token: this.resetToken,
2691
3596
  new_password: newPassword,
2692
3597
  });
2693
- // Clean up URL (remove verify_token from query string)
3598
+ // Clean up URL (remove reset_token from query string)
2694
3599
  const url = new URL(window.location.href);
2695
- url.searchParams.delete('verify_token');
3600
+ url.searchParams.delete('reset_token');
2696
3601
  window.history.replaceState({}, document.title, url.toString());
2697
3602
  // Clear reset token
2698
3603
  this.resetToken = null;
2699
3604
  // Show success message
2700
- this.showMessage('Password Reset Successful', 'Your password has been updated successfully. You can now log in with your new password.');
3605
+ this.showSuccess('Password Reset Successful', 'Your password has been updated successfully. You can now log in with your new password.');
2701
3606
  }
2702
3607
  catch (error) {
2703
3608
  // Handle error
2704
3609
  const errorMessage = error instanceof Error ? error.message : 'Failed to reset password';
2705
- this.showError(errorMessage);
3610
+ this.showError('Reset Failed', errorMessage);
2706
3611
  if (this.options.onError) {
2707
3612
  this.options.onError(error);
2708
3613
  }
@@ -2714,24 +3619,89 @@ class AuthModal {
2714
3619
  btnLoader?.classList.add('hidden');
2715
3620
  }
2716
3621
  }
2717
- showMessage(title, message) {
2718
- if (!this.modal)
3622
+ async handleBindInviteCode(e) {
3623
+ e.preventDefault();
3624
+ const codeInput = this.modal?.querySelector('#inviteCodeInput');
3625
+ const submitBtn = this.modal?.querySelector('#bindInviteCodeButton');
3626
+ const btnText = submitBtn?.querySelector('.btn-text');
3627
+ const btnLoader = submitBtn?.querySelector('.btn-loader');
3628
+ if (!codeInput || !submitBtn)
3629
+ return;
3630
+ const inviteCode = codeInput.value.trim();
3631
+ // Validate invite code
3632
+ if (!inviteCode) {
3633
+ this.showError('Invalid Input', 'Please enter an invitation code');
3634
+ return;
3635
+ }
3636
+ // Validate temp user ID exists
3637
+ if (!this.tempUserId) {
3638
+ this.showError('Session Error', 'User ID is missing. Please try logging in again.');
2719
3639
  return;
2720
- const messageTitle = this.modal.querySelector('#messageTitle');
2721
- const messageText = this.modal.querySelector('#messageText');
2722
- const messageButton = this.modal.querySelector('#messageButton');
2723
- if (messageTitle)
2724
- messageTitle.textContent = title;
2725
- if (messageText)
2726
- messageText.textContent = message;
2727
- messageButton?.addEventListener('click', () => {
2728
- this.hide();
2729
- }, { once: true });
2730
- this.switchView('message');
2731
- }
2732
- showError(message) {
2733
- // Simple error display - you can enhance this
2734
- alert(message);
3640
+ }
3641
+ try {
3642
+ // Show loading state
3643
+ submitBtn.disabled = true;
3644
+ btnText?.classList.add('hidden');
3645
+ btnLoader?.classList.remove('hidden');
3646
+ // Call bind invite code API
3647
+ const response = await this.client.bindInviteCode({
3648
+ user_id: this.tempUserId,
3649
+ invite_code: inviteCode,
3650
+ });
3651
+ // Handle success - auto login with returned token
3652
+ if (response.success && response.data.token) {
3653
+ const { token, refreshToken, user } = response.data;
3654
+ // Store tokens
3655
+ if (this.options.onLoginSuccess) {
3656
+ this.options.onLoginSuccess(token, user);
3657
+ }
3658
+ // Auto-set token in client
3659
+ this.client.setToken(token);
3660
+ // Show success message
3661
+ this.showSuccess('Account Activated!', 'Your account has been successfully activated. Welcome!');
3662
+ // Clear temp user data
3663
+ this.tempUserId = null;
3664
+ this.tempUserEmail = null;
3665
+ }
3666
+ else {
3667
+ throw new Error('Invalid response from server');
3668
+ }
3669
+ }
3670
+ catch (error) {
3671
+ // Handle errors
3672
+ const errorMessage = error.response?.data?.error || error.message || 'Failed to activate account';
3673
+ this.showError('Activation Failed', errorMessage);
3674
+ if (this.options.onError) {
3675
+ this.options.onError(error);
3676
+ }
3677
+ }
3678
+ finally {
3679
+ // Reset loading state
3680
+ submitBtn.disabled = false;
3681
+ btnText?.classList.remove('hidden');
3682
+ btnLoader?.classList.add('hidden');
3683
+ }
3684
+ }
3685
+ showSuccess(title, message) {
3686
+ Toast.success(title, message);
3687
+ // Also hide modal after short delay for better UX
3688
+ setTimeout(() => this.hide(), 1500);
3689
+ }
3690
+ showError(title, message) {
3691
+ Toast.error(title, message);
3692
+ }
3693
+ showWarning(title, message) {
3694
+ Toast.warning(title, message);
3695
+ }
3696
+ showInfo(title, message) {
3697
+ Toast.info(title, message);
3698
+ }
3699
+ /**
3700
+ * @deprecated Use showSuccess, showError, showWarning, or showInfo instead
3701
+ */
3702
+ showMessage(title, message) {
3703
+ // Fallback for backward compatibility
3704
+ Toast.info(title, message);
2735
3705
  }
2736
3706
  // ============================================================================
2737
3707
  // OAuth Methods
@@ -2749,7 +3719,7 @@ class AuthModal {
2749
3719
  async startOAuthFlow(provider) {
2750
3720
  try {
2751
3721
  // Get the return URL (where user should be redirected after OAuth)
2752
- const return_url = this.options.returnUrl || window.location.origin;
3722
+ const return_url = this.options.returnUrl || window.location.href;
2753
3723
  // Call backend to get OAuth authorization URL
2754
3724
  let authorizeUrl;
2755
3725
  switch (provider) {
@@ -2773,7 +3743,7 @@ class AuthModal {
2773
3743
  }
2774
3744
  catch (error) {
2775
3745
  const err = error instanceof Error ? error : new Error(`${provider} OAuth failed`);
2776
- this.showError(err.message);
3746
+ this.showError('OAuth Failed', err.message);
2777
3747
  if (this.options.onError) {
2778
3748
  this.options.onError(err);
2779
3749
  }
@@ -2806,6 +3776,62 @@ class AuthModal {
2806
3776
  token,
2807
3777
  };
2808
3778
  }
3779
+ /**
3780
+ * Handle invite code required scenario (static method for custom UI)
3781
+ *
3782
+ * When backend redirects to return_url?error_code=INVITE_CODE_REQUIRED&user_id=xxx&email=xxx,
3783
+ * call this method to show the invite code form or handle it custom way.
3784
+ *
3785
+ * @param options - Auth modal options
3786
+ * @param customHandler - Optional custom handler for invite code requirement
3787
+ * @returns Object with userId and email if invite code is required, null otherwise
3788
+ *
3789
+ * @example
3790
+ * // Auto-show AuthModal invite code form
3791
+ * const modal = new AuthModal(options);
3792
+ * AuthModal.handleInviteCodeRequired(options);
3793
+ *
3794
+ * @example
3795
+ * // Custom UI handling
3796
+ * const result = AuthModal.handleInviteCodeRequired(options, (userId, email) => {
3797
+ * // Show your custom invite code input UI
3798
+ * const code = prompt('Enter invitation code:');
3799
+ * if (code) {
3800
+ * options.client.bindInviteCode({ user_id: userId, invite_code: code })
3801
+ * .then(res => {
3802
+ * localStorage.setItem('token', res.data.token);
3803
+ * window.location.reload();
3804
+ * });
3805
+ * }
3806
+ * });
3807
+ */
3808
+ static handleInviteCodeRequired(options, customHandler) {
3809
+ const urlParams = new URLSearchParams(window.location.search);
3810
+ const errorCode = urlParams.get('error_code');
3811
+ const userId = urlParams.get('user_id');
3812
+ const email = urlParams.get('email');
3813
+ if (errorCode !== 'INVITE_CODE_REQUIRED' || !userId) {
3814
+ return null; // Not an invite code required scenario
3815
+ }
3816
+ // Clean up URL
3817
+ const url = new URL(window.location.href);
3818
+ url.searchParams.delete('error_code');
3819
+ url.searchParams.delete('user_id');
3820
+ url.searchParams.delete('email');
3821
+ window.history.replaceState({}, document.title, url.toString());
3822
+ // Call custom handler if provided
3823
+ if (customHandler) {
3824
+ customHandler(userId, email || '');
3825
+ }
3826
+ else if (options.onInviteCodeRequired) {
3827
+ // Call the callback option
3828
+ options.onInviteCodeRequired(userId, email || '');
3829
+ }
3830
+ return {
3831
+ userId,
3832
+ email: email || '',
3833
+ };
3834
+ }
2809
3835
  }
2810
3836
  /**
2811
3837
  * Create and show auth modal
@@ -2820,6 +3846,7 @@ exports.AuthProvider = AuthProvider;
2820
3846
  exports.BuiltInHooks = BuiltInHooks;
2821
3847
  exports.ENVIRONMENT_CONFIGS = ENVIRONMENT_CONFIGS;
2822
3848
  exports.SeaVerseBackendAPIClient = SeaVerseBackendAPIClient;
3849
+ exports.Toast = Toast;
2823
3850
  exports.createAuthModal = createAuthModal;
2824
3851
  exports.detectEnvironment = detectEnvironment;
2825
3852
  exports.getEnvironmentConfig = getEnvironmentConfig;