mnfst 0.5.224 → 0.5.226
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/lib/manifest.appwrite.auth.js +280 -27
- package/lib/manifest.appwrite.data.js +1 -1
- package/lib/manifest.d.ts +1 -1
- package/lib/manifest.data.js +103 -36
- package/lib/manifest.integrity.json +4 -5
- package/lib/manifest.sw.js +1 -1
- package/package.json +3 -2
- package/lib/manifest.resize.css +0 -194
- package/lib/manifest.resize.js +0 -488
|
@@ -70,6 +70,7 @@ async function getAppwriteConfig() {
|
|
|
70
70
|
|
|
71
71
|
const magicEnabled = authMethods.includes("magic");
|
|
72
72
|
const otpEnabled = authMethods.includes("otp");
|
|
73
|
+
const phoneEnabled = authMethods.includes("phone");
|
|
73
74
|
const oauthEnabled = authMethods.includes("oauth");
|
|
74
75
|
|
|
75
76
|
// Teams (presence of teams object enables it)
|
|
@@ -92,7 +93,7 @@ async function getAppwriteConfig() {
|
|
|
92
93
|
: toBool(appwriteConfig.auth.teams.authenticated);
|
|
93
94
|
|
|
94
95
|
// Guest upgrade: preserve the anonymous account + teams on sign-in (magic/oauth;
|
|
95
|
-
// OTP can't convert anonymous accounts). Defaults to guestTeams.
|
|
96
|
+
// email/phone OTP can't convert anonymous accounts). Defaults to guestTeams.
|
|
96
97
|
const guestUpgrade = appwriteConfig.auth?.guestUpgrade !== undefined
|
|
97
98
|
? toBool(appwriteConfig.auth.guestUpgrade)
|
|
98
99
|
: guestTeams;
|
|
@@ -135,6 +136,7 @@ async function getAppwriteConfig() {
|
|
|
135
136
|
anonymous: guestAuto, // back-compat alias
|
|
136
137
|
magic: magicEnabled,
|
|
137
138
|
otp: otpEnabled,
|
|
139
|
+
phone: phoneEnabled,
|
|
138
140
|
oauth: oauthEnabled,
|
|
139
141
|
teams: teamsEnabled,
|
|
140
142
|
permanentTeams: permanentTeams,
|
|
@@ -253,6 +255,19 @@ function initializeAuthStore() {
|
|
|
253
255
|
'deviceBrand', 'deviceModel', 'countryCode', 'countryName'
|
|
254
256
|
];
|
|
255
257
|
|
|
258
|
+
// User id in a mirrored state ('' / null when signed out)
|
|
259
|
+
function mirroredUserId(raw) {
|
|
260
|
+
try { const saved = JSON.parse(raw || 'null'); return saved?.isAuthenticated ? (saved.user?.$id || null) : null; } catch { return null; }
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Identity this tab is showing data for: while unverified, the last one mirrored
|
|
264
|
+
function shownUserId(store, mirrored) {
|
|
265
|
+
if (store.user?.$id) return store.user.$id;
|
|
266
|
+
if (!store._sessionUnverified) return null;
|
|
267
|
+
if (mirrored === undefined) { try { mirrored = localStorage.getItem(STORAGE_KEY); } catch { mirrored = null; } }
|
|
268
|
+
return mirroredUserId(mirrored);
|
|
269
|
+
}
|
|
270
|
+
|
|
256
271
|
function sanitizeSessionForStorage(session) {
|
|
257
272
|
if (!session || typeof session !== 'object') return session;
|
|
258
273
|
const safe = {};
|
|
@@ -269,7 +284,8 @@ function initializeAuthStore() {
|
|
|
269
284
|
const state = JSON.parse(e.newValue);
|
|
270
285
|
const store = Alpine.store('auth');
|
|
271
286
|
if (store) {
|
|
272
|
-
const prevId = store.
|
|
287
|
+
const prevId = shownUserId(store, e.oldValue);
|
|
288
|
+
const wasAuthenticated = store.isAuthenticated === true;
|
|
273
289
|
// Update store state from other tab
|
|
274
290
|
store._authEpoch++;
|
|
275
291
|
if (state.isAuthenticated) markVerified(store);
|
|
@@ -281,6 +297,8 @@ function initializeAuthStore() {
|
|
|
281
297
|
store.magicLinkExpired = state.magicLinkExpired || false;
|
|
282
298
|
store.otpSent = state.otpSent || false;
|
|
283
299
|
store.otpExpired = state.otpExpired || false;
|
|
300
|
+
// A code row shown here must be verifiable here
|
|
301
|
+
if (state.otpSent && state.otpUserId) store._otpUserId = state.otpUserId;
|
|
284
302
|
store.error = state.error;
|
|
285
303
|
// Identity ended or changed in another tab: scoped data must drop this tab's rows
|
|
286
304
|
const nextId = state.isAuthenticated ? (state.user?.$id || null) : null;
|
|
@@ -290,6 +308,9 @@ function initializeAuthStore() {
|
|
|
290
308
|
store.currentTeam = null;
|
|
291
309
|
}
|
|
292
310
|
window.dispatchEvent(new CustomEvent('manifest:auth:session-cleared'));
|
|
311
|
+
} else if (nextId && !wasAuthenticated) {
|
|
312
|
+
// Signed in from another tab: this tab's identity-bound data reloads
|
|
313
|
+
window.dispatchEvent(new CustomEvent(state.isAnonymous ? 'manifest:auth:anonymous' : 'manifest:auth:login', { detail: { user: state.user, crossTab: true } }));
|
|
293
314
|
}
|
|
294
315
|
}
|
|
295
316
|
} catch (error) {
|
|
@@ -312,6 +333,7 @@ function initializeAuthStore() {
|
|
|
312
333
|
magicLinkExpired: store.magicLinkExpired,
|
|
313
334
|
otpSent: store.otpSent,
|
|
314
335
|
otpExpired: store.otpExpired,
|
|
336
|
+
otpUserId: store.otpSent ? store._otpUserId : null,
|
|
315
337
|
error: store.error
|
|
316
338
|
};
|
|
317
339
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
|
@@ -329,10 +351,11 @@ function initializeAuthStore() {
|
|
|
329
351
|
error: null,
|
|
330
352
|
magicLinkSent: false,
|
|
331
353
|
magicLinkExpired: false,
|
|
332
|
-
otpSent: false, //
|
|
333
|
-
otpExpired: false, //
|
|
334
|
-
otpPhrase: null, // Email OTP: security phrase to display (when enabled)
|
|
335
|
-
_otpUserId: null, //
|
|
354
|
+
otpSent: false, // OTP (email/phone): a code has been sent and is awaiting entry
|
|
355
|
+
otpExpired: false, // OTP (email/phone): the entered code was wrong/expired
|
|
356
|
+
otpPhrase: null, // Email OTP: security phrase to display (when enabled; phone has none)
|
|
357
|
+
_otpUserId: null, // OTP: userId returned by createEmailToken/createPhoneToken, used by verifyOTP
|
|
358
|
+
_otpMigrationTicket: null, // OTP: { ticket, userId } guest-migration ticket, kept across failed verify attempts
|
|
336
359
|
teams: [], // List of user's teams
|
|
337
360
|
currentTeam: null, // Currently selected/active team
|
|
338
361
|
_teamsPollInterval: null, // Interval ID for teams polling (deprecated, use realtime instead)
|
|
@@ -711,9 +734,12 @@ function initializeAuthStore() {
|
|
|
711
734
|
} catch (error) {
|
|
712
735
|
if (!current()) return;
|
|
713
736
|
if (!isNoSession(error)) { scheduleRecheck(this); return; }
|
|
737
|
+
const prevId = shownUserId(this);
|
|
714
738
|
this._clearIdentity();
|
|
715
739
|
markVerified(this);
|
|
716
740
|
syncStateToStorage(this);
|
|
741
|
+
// Rows shown for the last-known user drop
|
|
742
|
+
if (prevId) window.dispatchEvent(new CustomEvent('manifest:auth:session-cleared'));
|
|
717
743
|
if (this._guestAuto && this._createAnonymousSession) await this._createAnonymousSession();
|
|
718
744
|
return;
|
|
719
745
|
} finally {
|
|
@@ -905,6 +931,9 @@ function initializeAuthStore() {
|
|
|
905
931
|
return { success: false, error: 'Appwrite not configured' };
|
|
906
932
|
}
|
|
907
933
|
|
|
934
|
+
// A ticket kept from a failed guest OTP must not outlive an explicit logout
|
|
935
|
+
this._otpMigrationTicket = null;
|
|
936
|
+
|
|
908
937
|
// If not authenticated, nothing to logout from
|
|
909
938
|
if (!this.isAuthenticated) {
|
|
910
939
|
return { success: true };
|
|
@@ -933,7 +962,7 @@ function initializeAuthStore() {
|
|
|
933
962
|
this.magicLinkSent = false;
|
|
934
963
|
this.magicLinkExpired = false;
|
|
935
964
|
|
|
936
|
-
// Clear
|
|
965
|
+
// Clear OTP flags (email/phone)
|
|
937
966
|
this.otpSent = false;
|
|
938
967
|
this.otpExpired = false;
|
|
939
968
|
this.otpPhrase = null;
|
|
@@ -1078,12 +1107,18 @@ function initializeAuthStore() {
|
|
|
1078
1107
|
return this.user;
|
|
1079
1108
|
} catch (error) {
|
|
1080
1109
|
if (!isNoSession(error)) throw error;
|
|
1110
|
+
const prevId = shownUserId(this);
|
|
1081
1111
|
markVerified(this);
|
|
1082
1112
|
this.isAuthenticated = false;
|
|
1083
1113
|
this.isAnonymous = false;
|
|
1084
1114
|
this.user = null;
|
|
1085
1115
|
this.session = null;
|
|
1086
1116
|
syncStateToStorage(this);
|
|
1117
|
+
if (prevId) {
|
|
1118
|
+
this.teams = [];
|
|
1119
|
+
this.currentTeam = null;
|
|
1120
|
+
window.dispatchEvent(new CustomEvent('manifest:auth:session-cleared'));
|
|
1121
|
+
}
|
|
1087
1122
|
throw error;
|
|
1088
1123
|
}
|
|
1089
1124
|
}
|
|
@@ -6294,7 +6329,8 @@ window.ManifestAppwriteAuthMagicLinks = {
|
|
|
6294
6329
|
/* Auth email OTP (one-time passcode) */
|
|
6295
6330
|
|
|
6296
6331
|
// Two-step in-page flow (no redirect): createEmailOTP(email) emails a code + returns
|
|
6297
|
-
// a userId, verifyOTP(code) creates the session.
|
|
6332
|
+
// a userId, verifyOTP(code) creates the session. verifyOTP/submitOTP are shared
|
|
6333
|
+
// with phone OTP (users.phone.js).
|
|
6298
6334
|
// Gotcha: Appwrite can't convert an anonymous guest via OTP — a guest verifying an OTP
|
|
6299
6335
|
// gets a fresh account (guest teams lost). Use magic links for guest upgrade.
|
|
6300
6336
|
|
|
@@ -6364,17 +6400,20 @@ function initializeEmailOTP() {
|
|
|
6364
6400
|
this._appwrite = await config.getAppwriteClient();
|
|
6365
6401
|
}
|
|
6366
6402
|
if (!this._appwrite) {
|
|
6367
|
-
|
|
6403
|
+
this.error = 'Appwrite not configured';
|
|
6404
|
+
return { success: false, error: this.error };
|
|
6368
6405
|
}
|
|
6369
6406
|
|
|
6370
6407
|
// Don't allow OTP request if already signed in (non-anonymous)
|
|
6371
6408
|
if (this.isAuthenticated && !this.isAnonymous) {
|
|
6372
|
-
|
|
6409
|
+
this.error = 'Already signed in. Please logout first.';
|
|
6410
|
+
return { success: false, error: this.error };
|
|
6373
6411
|
}
|
|
6374
6412
|
|
|
6375
6413
|
const appwriteConfig = await config.getAppwriteConfig();
|
|
6376
6414
|
if (appwriteConfig && !appwriteConfig.otp) {
|
|
6377
|
-
|
|
6415
|
+
this.error = 'Email OTP authentication is not enabled';
|
|
6416
|
+
return { success: false, error: this.error };
|
|
6378
6417
|
}
|
|
6379
6418
|
|
|
6380
6419
|
// OTP can't convert a guest — warn so lost guest teams aren't a surprise.
|
|
@@ -6384,10 +6423,8 @@ function initializeEmailOTP() {
|
|
|
6384
6423
|
|
|
6385
6424
|
const account = this._appwrite.account;
|
|
6386
6425
|
if (typeof account.createEmailToken !== 'function') {
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
error: 'Email OTP method not available. Please ensure you are using a recent Appwrite SDK.'
|
|
6390
|
-
};
|
|
6426
|
+
this.error = 'Email OTP method not available. Please ensure you are using a recent Appwrite SDK.';
|
|
6427
|
+
return { success: false, error: this.error };
|
|
6391
6428
|
}
|
|
6392
6429
|
|
|
6393
6430
|
this.inProgress = true;
|
|
@@ -6433,7 +6470,8 @@ function initializeEmailOTP() {
|
|
|
6433
6470
|
const { email, inputEl, dataObj } = resolveEmailInput(emailInputOrRef);
|
|
6434
6471
|
|
|
6435
6472
|
if (!email || !email.trim()) {
|
|
6436
|
-
|
|
6473
|
+
this.error = 'Email is required';
|
|
6474
|
+
return { success: false, error: this.error };
|
|
6437
6475
|
}
|
|
6438
6476
|
|
|
6439
6477
|
const result = await this.createEmailOTP(email.trim(), options);
|
|
@@ -6469,26 +6507,33 @@ function initializeEmailOTP() {
|
|
|
6469
6507
|
|
|
6470
6508
|
this.inProgress = true;
|
|
6471
6509
|
this.error = null;
|
|
6510
|
+
let guestEnded = false;
|
|
6472
6511
|
|
|
6473
6512
|
try {
|
|
6474
6513
|
const appwriteConfig = await config.getAppwriteConfig();
|
|
6475
6514
|
const wasGuest = this.isAnonymous;
|
|
6476
6515
|
|
|
6477
|
-
// Guest team carryover: issue the migration ticket
|
|
6478
|
-
//
|
|
6479
|
-
|
|
6516
|
+
// Guest team carryover: issue the migration ticket while the guest session
|
|
6517
|
+
// still authenticates. Kept on the store, bound to this code's account, so a
|
|
6518
|
+
// retry after a wrong code redeems it and no other sign-in can.
|
|
6480
6519
|
if (wasGuest && appwriteConfig?.guestMigrationFunctionId && this._callGuestMigration) {
|
|
6481
6520
|
const prep = await this._callGuestMigration('/prepare', {});
|
|
6482
|
-
if (prep?.ok && prep.ticket)
|
|
6521
|
+
if (prep?.ok && prep.ticket) {
|
|
6522
|
+
this._otpMigrationTicket = { ticket: prep.ticket, userId: this._otpUserId };
|
|
6523
|
+
}
|
|
6483
6524
|
}
|
|
6525
|
+
const kept = this._otpMigrationTicket;
|
|
6526
|
+
const migrationTicket = kept?.userId === this._otpUserId ? kept.ticket : null;
|
|
6484
6527
|
|
|
6485
|
-
// Appwrite
|
|
6486
|
-
//
|
|
6528
|
+
// Appwrite refuses createSession while any session is active, before it
|
|
6529
|
+
// checks the code, so the guest session must go first.
|
|
6487
6530
|
if (this.session && this.isAnonymous) {
|
|
6488
6531
|
try {
|
|
6489
6532
|
await this._appwrite.account.deleteSession(this.session.$id);
|
|
6533
|
+
guestEnded = true;
|
|
6490
6534
|
} catch (deleteError) {
|
|
6491
|
-
|
|
6535
|
+
const status = deleteError?.code || deleteError?.statusCode;
|
|
6536
|
+
guestEnded = status === 401 || status === 404;
|
|
6492
6537
|
}
|
|
6493
6538
|
}
|
|
6494
6539
|
|
|
@@ -6511,6 +6556,7 @@ function initializeEmailOTP() {
|
|
|
6511
6556
|
|
|
6512
6557
|
// Redeem the migration ticket as the new account to carry teams over.
|
|
6513
6558
|
// Best-effort; failure never blocks the already-successful sign-in.
|
|
6559
|
+
this._otpMigrationTicket = null;
|
|
6514
6560
|
if (migrationTicket && this._callGuestMigration) {
|
|
6515
6561
|
await this._callGuestMigration('/commit', { ticket: migrationTicket });
|
|
6516
6562
|
}
|
|
@@ -6536,7 +6582,7 @@ function initializeEmailOTP() {
|
|
|
6536
6582
|
} catch (error) {
|
|
6537
6583
|
const errorMessage = error.message || '';
|
|
6538
6584
|
const errorCode = error.code || error.statusCode || '';
|
|
6539
|
-
const isExpiredOrInvalid = errorMessage && (
|
|
6585
|
+
const isExpiredOrInvalid = error.type !== 'user_session_already_exists' && errorMessage && (
|
|
6540
6586
|
errorMessage.includes('expired') ||
|
|
6541
6587
|
errorMessage.includes('Invalid token') ||
|
|
6542
6588
|
errorMessage.includes('invalid') ||
|
|
@@ -6546,14 +6592,30 @@ function initializeEmailOTP() {
|
|
|
6546
6592
|
|
|
6547
6593
|
this.otpExpired = !!isExpiredOrInvalid;
|
|
6548
6594
|
this.error = isExpiredOrInvalid ? null : error.message;
|
|
6549
|
-
|
|
6550
|
-
|
|
6595
|
+
|
|
6596
|
+
// Guest session is gone though sign-in failed: drop its identity and teams.
|
|
6597
|
+
// otpSent, the code's userId and the migration ticket stay for a retry.
|
|
6598
|
+
if (guestEnded) {
|
|
6599
|
+
this.isAuthenticated = false;
|
|
6600
|
+
this.isAnonymous = false;
|
|
6601
|
+
this.user = null;
|
|
6602
|
+
this.session = null;
|
|
6603
|
+
if (this._resetTeamsState) {
|
|
6604
|
+
this._resetTeamsState();
|
|
6605
|
+
}
|
|
6606
|
+
}
|
|
6551
6607
|
|
|
6552
6608
|
if (this._syncStateToStorage) {
|
|
6553
6609
|
this._syncStateToStorage(this);
|
|
6554
6610
|
}
|
|
6555
6611
|
|
|
6556
|
-
|
|
6612
|
+
if (guestEnded) {
|
|
6613
|
+
window.dispatchEvent(new CustomEvent('manifest:auth:session-cleared', {
|
|
6614
|
+
detail: { reason: 'otp-failed' }
|
|
6615
|
+
}));
|
|
6616
|
+
}
|
|
6617
|
+
|
|
6618
|
+
return { success: false, error: error.message, guestEnded };
|
|
6557
6619
|
} finally {
|
|
6558
6620
|
this.inProgress = false;
|
|
6559
6621
|
}
|
|
@@ -6607,6 +6669,197 @@ window.ManifestAppwriteAuthEmailOTP = {
|
|
|
6607
6669
|
};
|
|
6608
6670
|
|
|
6609
6671
|
|
|
6672
|
+
/* Auth phone OTP (SMS one-time passcode) */
|
|
6673
|
+
|
|
6674
|
+
// Two-step in-page flow (no redirect): createPhoneOTP(phone) texts a code + returns
|
|
6675
|
+
// a userId, then the shared verifyOTP(code)/submitOTP() (users.otp.js) creates the
|
|
6676
|
+
// session. Phone numbers must be E.164 (+ country code).
|
|
6677
|
+
// Gotcha: like email OTP, Appwrite can't convert an anonymous guest via phone OTP —
|
|
6678
|
+
// a guest verifying a code gets a fresh account. Use magic links for guest upgrade.
|
|
6679
|
+
|
|
6680
|
+
function initializePhoneOTP() {
|
|
6681
|
+
if (typeof Alpine === 'undefined') {
|
|
6682
|
+
return;
|
|
6683
|
+
}
|
|
6684
|
+
|
|
6685
|
+
const config = window.ManifestAppwriteAuthConfig;
|
|
6686
|
+
if (!config) {
|
|
6687
|
+
return;
|
|
6688
|
+
}
|
|
6689
|
+
|
|
6690
|
+
// Resolve a phone from an input/selector/{ phone } object/string, or auto-find
|
|
6691
|
+
// the nearest tel input. Returns { phone, inputEl, dataObj }.
|
|
6692
|
+
function resolvePhoneInput(phoneInputOrRef) {
|
|
6693
|
+
let phone = null;
|
|
6694
|
+
let inputEl = null;
|
|
6695
|
+
let dataObj = null;
|
|
6696
|
+
|
|
6697
|
+
if (phoneInputOrRef === undefined || phoneInputOrRef === null) {
|
|
6698
|
+
let eventTarget = (typeof window !== 'undefined' && window.event) ? window.event.target : null;
|
|
6699
|
+
if (eventTarget) {
|
|
6700
|
+
const form = eventTarget.closest('form');
|
|
6701
|
+
const scope = form || eventTarget.parentElement;
|
|
6702
|
+
if (scope) {
|
|
6703
|
+
inputEl = scope.querySelector('input[type="tel"]');
|
|
6704
|
+
if (inputEl) phone = inputEl.value;
|
|
6705
|
+
}
|
|
6706
|
+
}
|
|
6707
|
+
if (!inputEl) {
|
|
6708
|
+
inputEl = document.querySelector('input[type="tel"]');
|
|
6709
|
+
if (inputEl) phone = inputEl.value;
|
|
6710
|
+
}
|
|
6711
|
+
} else if (typeof phoneInputOrRef === 'string') {
|
|
6712
|
+
try {
|
|
6713
|
+
const element = document.querySelector(phoneInputOrRef);
|
|
6714
|
+
if (element && element.tagName === 'INPUT' && element.type === 'tel') {
|
|
6715
|
+
inputEl = element;
|
|
6716
|
+
phone = element.value;
|
|
6717
|
+
} else {
|
|
6718
|
+
phone = phoneInputOrRef; // Treat as a direct phone string
|
|
6719
|
+
}
|
|
6720
|
+
} catch (e) {
|
|
6721
|
+
phone = phoneInputOrRef; // Invalid selector -> treat as phone string
|
|
6722
|
+
}
|
|
6723
|
+
} else if (phoneInputOrRef && typeof phoneInputOrRef === 'object') {
|
|
6724
|
+
if (phoneInputOrRef.tagName === 'INPUT' || phoneInputOrRef.matches?.('input[type="tel"]')) {
|
|
6725
|
+
inputEl = phoneInputOrRef;
|
|
6726
|
+
phone = inputEl.value;
|
|
6727
|
+
} else if ('phone' in phoneInputOrRef) {
|
|
6728
|
+
phone = phoneInputOrRef.phone;
|
|
6729
|
+
dataObj = phoneInputOrRef;
|
|
6730
|
+
}
|
|
6731
|
+
}
|
|
6732
|
+
|
|
6733
|
+
return { phone, inputEl, dataObj };
|
|
6734
|
+
}
|
|
6735
|
+
|
|
6736
|
+
const waitForStore = () => {
|
|
6737
|
+
const store = Alpine.store('auth');
|
|
6738
|
+
if (store && !store.createPhoneOTP) {
|
|
6739
|
+
// Step 1: text a passcode. Verification reuses verifyOTP/submitOTP.
|
|
6740
|
+
store.createPhoneOTP = async function (phone) {
|
|
6741
|
+
if (!this._appwrite) {
|
|
6742
|
+
this._appwrite = await config.getAppwriteClient();
|
|
6743
|
+
}
|
|
6744
|
+
if (!this._appwrite) {
|
|
6745
|
+
this.error = 'Appwrite not configured';
|
|
6746
|
+
return { success: false, error: this.error };
|
|
6747
|
+
}
|
|
6748
|
+
|
|
6749
|
+
// Don't allow OTP request if already signed in (non-anonymous)
|
|
6750
|
+
if (this.isAuthenticated && !this.isAnonymous) {
|
|
6751
|
+
this.error = 'Already signed in. Please logout first.';
|
|
6752
|
+
return { success: false, error: this.error };
|
|
6753
|
+
}
|
|
6754
|
+
|
|
6755
|
+
const appwriteConfig = await config.getAppwriteConfig();
|
|
6756
|
+
if (appwriteConfig && !appwriteConfig.phone) {
|
|
6757
|
+
this.error = 'Phone OTP authentication is not enabled';
|
|
6758
|
+
return { success: false, error: this.error };
|
|
6759
|
+
}
|
|
6760
|
+
|
|
6761
|
+
// Normalize to E.164: strip separators, require the country code.
|
|
6762
|
+
const normalized = String(phone).replace(/[\s().-]/g, '');
|
|
6763
|
+
if (!/^\+\d{6,15}$/.test(normalized)) {
|
|
6764
|
+
this.error = 'Phone number must be in international format, e.g. +14155550123';
|
|
6765
|
+
return { success: false, error: this.error };
|
|
6766
|
+
}
|
|
6767
|
+
|
|
6768
|
+
// Phone OTP can't convert a guest — warn so lost guest teams aren't a surprise.
|
|
6769
|
+
if (this.isAnonymous && appwriteConfig?.guestUpgrade) {
|
|
6770
|
+
console.warn('[Manifest Appwrite Auth] Phone OTP cannot upgrade a guest account (Appwrite limitation); the guest session and any guest-created teams will be replaced. Use magic links for guest upgrade.');
|
|
6771
|
+
}
|
|
6772
|
+
|
|
6773
|
+
const account = this._appwrite.account;
|
|
6774
|
+
if (typeof account.createPhoneToken !== 'function') {
|
|
6775
|
+
this.error = 'Phone OTP method not available. Please ensure you are using a recent Appwrite SDK.';
|
|
6776
|
+
return { success: false, error: this.error };
|
|
6777
|
+
}
|
|
6778
|
+
|
|
6779
|
+
this.inProgress = true;
|
|
6780
|
+
this.error = null;
|
|
6781
|
+
this.otpExpired = false;
|
|
6782
|
+
|
|
6783
|
+
try {
|
|
6784
|
+
const uniqueId = (window.Appwrite?.ID?.unique) ? window.Appwrite.ID.unique() : 'unique()';
|
|
6785
|
+
const token = await account.createPhoneToken(uniqueId, normalized);
|
|
6786
|
+
|
|
6787
|
+
// Stash the userId Appwrite assigned; verifyOTP needs it to complete login.
|
|
6788
|
+
this._otpUserId = token.userId;
|
|
6789
|
+
this.otpPhrase = null; // phone tokens have no security phrase
|
|
6790
|
+
this.otpSent = true;
|
|
6791
|
+
this.otpExpired = false;
|
|
6792
|
+
this.error = null;
|
|
6793
|
+
|
|
6794
|
+
window.dispatchEvent(new CustomEvent('manifest:auth:otp-sent', {
|
|
6795
|
+
detail: { phone: normalized }
|
|
6796
|
+
}));
|
|
6797
|
+
|
|
6798
|
+
return { success: true, message: 'OTP sent by SMS' };
|
|
6799
|
+
} catch (error) {
|
|
6800
|
+
// Appwrite returns 501 when phone auth/SMS isn't set up — surface an
|
|
6801
|
+
// actionable message rather than the raw error.
|
|
6802
|
+
const code = error.code || error.statusCode;
|
|
6803
|
+
const notEnabled = code === 501 || /not implemented/i.test(error.message || '');
|
|
6804
|
+
this.error = notEnabled
|
|
6805
|
+
? 'Phone OTP is not enabled for this Appwrite project. Enable Phone under Auth → Settings and configure an SMS provider under Messaging.'
|
|
6806
|
+
: error.message;
|
|
6807
|
+
this.otpSent = false;
|
|
6808
|
+
this.otpExpired = false;
|
|
6809
|
+
return { success: false, error: this.error };
|
|
6810
|
+
} finally {
|
|
6811
|
+
this.inProgress = false;
|
|
6812
|
+
}
|
|
6813
|
+
};
|
|
6814
|
+
|
|
6815
|
+
// Convenience: resolve the phone from an input/selector/object/string and send.
|
|
6816
|
+
// Clears the phone input on success (mirrors sendEmailOTP).
|
|
6817
|
+
store.sendPhoneOTP = async function (phoneInputOrRef) {
|
|
6818
|
+
const { phone, inputEl, dataObj } = resolvePhoneInput(phoneInputOrRef);
|
|
6819
|
+
|
|
6820
|
+
if (!phone || !String(phone).trim()) {
|
|
6821
|
+
this.error = 'Phone number is required';
|
|
6822
|
+
return { success: false, error: this.error };
|
|
6823
|
+
}
|
|
6824
|
+
|
|
6825
|
+
const result = await this.createPhoneOTP(String(phone).trim());
|
|
6826
|
+
|
|
6827
|
+
if (result.success) {
|
|
6828
|
+
Promise.resolve().then(() => {
|
|
6829
|
+
if (inputEl) {
|
|
6830
|
+
inputEl.value = '';
|
|
6831
|
+
inputEl.dispatchEvent(new Event('input', { bubbles: true }));
|
|
6832
|
+
} else if (dataObj) {
|
|
6833
|
+
dataObj.phone = '';
|
|
6834
|
+
}
|
|
6835
|
+
});
|
|
6836
|
+
}
|
|
6837
|
+
|
|
6838
|
+
return result;
|
|
6839
|
+
};
|
|
6840
|
+
} else if (!store) {
|
|
6841
|
+
setTimeout(waitForStore, 50);
|
|
6842
|
+
}
|
|
6843
|
+
};
|
|
6844
|
+
|
|
6845
|
+
setTimeout(waitForStore, 100);
|
|
6846
|
+
}
|
|
6847
|
+
|
|
6848
|
+
// Initialize when Alpine is ready
|
|
6849
|
+
document.addEventListener('alpine:init', () => {
|
|
6850
|
+
try {
|
|
6851
|
+
initializePhoneOTP();
|
|
6852
|
+
} catch (error) {
|
|
6853
|
+
// Failed to initialize phone OTP
|
|
6854
|
+
}
|
|
6855
|
+
});
|
|
6856
|
+
|
|
6857
|
+
// Export phone OTP interface
|
|
6858
|
+
window.ManifestAppwriteAuthPhoneOTP = {
|
|
6859
|
+
initialize: initializePhoneOTP
|
|
6860
|
+
};
|
|
6861
|
+
|
|
6862
|
+
|
|
6610
6863
|
/* Auth OAuth */
|
|
6611
6864
|
|
|
6612
6865
|
// Add OAuth methods to auth store
|
package/lib/manifest.d.ts
CHANGED
|
@@ -434,7 +434,7 @@ export interface ManifestTextEdit {
|
|
|
434
434
|
export type ManifestDirective =
|
|
435
435
|
| 'x-route' | 'x-anchors'
|
|
436
436
|
| 'x-icon' | 'x-svg' | 'x-markdown' | 'x-code' | 'x-code-group'
|
|
437
|
-
| 'x-toast' | 'x-tooltip' | 'x-carousel' | 'x-virtual' | 'x-
|
|
437
|
+
| 'x-toast' | 'x-tooltip' | 'x-carousel' | 'x-virtual' | 'x-computed' | 'x-defer'
|
|
438
438
|
| 'x-chart' | 'x-date' | 'x-color' | 'x-colorpicker' | 'x-combobox'
|
|
439
439
|
| 'x-export' | 'x-import' | 'x-pay' | 'x-files' | 'x-data-files'
|
|
440
440
|
| 'x-edit' | 'x-text-edit';
|