@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/README.md +410 -16
- package/dist/index.cjs +1073 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +392 -12
- package/dist/index.js +1073 -47
- package/dist/index.js.map +1 -1
- package/dist/toast.css +1 -0
- package/package.json +1 -1
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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('
|
|
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.
|
|
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
|
-
//
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
3598
|
+
// Clean up URL (remove reset_token from query string)
|
|
2694
3599
|
const url = new URL(window.location.href);
|
|
2695
|
-
url.searchParams.delete('
|
|
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.
|
|
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
|
-
|
|
2718
|
-
|
|
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
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
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.
|
|
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;
|