central-auth-js-sdk 1.2.4 → 1.2.6
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/core/api.js +14 -10
- package/login.js +21 -4
- package/package.json +1 -1
- package/signup.js +40 -5
- package/styles.css +96 -0
- package/ui/screens/email-verify.js +28 -1
- package/ui/screens/index.js +14 -3
- package/ui/screens/login/identifier-mobile.js +60 -34
- package/ui/screens/login/index.js +9 -4
- package/ui/screens/login/otp.js +194 -61
- package/ui/screens/mobile-entry.js +56 -31
- package/ui/screens/mobile-verify.js +184 -61
package/core/api.js
CHANGED
|
@@ -53,19 +53,23 @@ export function createApi(baseUrl = DEFAULT_BASE, options = {}) {
|
|
|
53
53
|
return request('POST', '/signup/start', { email });
|
|
54
54
|
},
|
|
55
55
|
|
|
56
|
-
/** POST /signup/email/verify — Email verify: OTP + names (entity auto-detected from email in backend) */
|
|
57
|
-
async signupEmailVerify({ email, otp, first_name, last_name }) {
|
|
58
|
-
|
|
56
|
+
/** POST /signup/email/verify — Email verify: OTP + names + org details (entity auto-detected from email in backend) */
|
|
57
|
+
async signupEmailVerify({ email, otp, first_name, last_name, team, sub_team, designation }) {
|
|
58
|
+
const payload = {
|
|
59
59
|
email,
|
|
60
60
|
otp,
|
|
61
61
|
first_name,
|
|
62
62
|
last_name,
|
|
63
|
-
}
|
|
63
|
+
};
|
|
64
|
+
if (team) payload.team = team;
|
|
65
|
+
if (sub_team) payload.sub_team = sub_team;
|
|
66
|
+
if (designation) payload.designation = designation;
|
|
67
|
+
return request('POST', '/signup/email/verify', payload);
|
|
64
68
|
},
|
|
65
69
|
|
|
66
70
|
/** POST /signup/mobile/send_otp — Mobile entry: send mobile OTP */
|
|
67
|
-
async signupMobileSendOtp(email, mobile) {
|
|
68
|
-
return request('POST', '/signup/mobile/send_otp', { email, mobile });
|
|
71
|
+
async signupMobileSendOtp(email, mobile, channel = 'sms') {
|
|
72
|
+
return request('POST', '/signup/mobile/send_otp', { email, mobile, channel });
|
|
69
73
|
},
|
|
70
74
|
|
|
71
75
|
/** POST /signup/mobile/verify — Mobile verify */
|
|
@@ -104,8 +108,8 @@ export function createApi(baseUrl = DEFAULT_BASE, options = {}) {
|
|
|
104
108
|
},
|
|
105
109
|
|
|
106
110
|
/** POST /login/google/mobile/send — after OAuth, send mobile OTP. app_identifier only when provided. */
|
|
107
|
-
async googleMobileSend(loginToken, mobile, appIdentifier) {
|
|
108
|
-
const body = { login_token: loginToken, mobile };
|
|
111
|
+
async googleMobileSend(loginToken, mobile, appIdentifier, channel = 'sms') {
|
|
112
|
+
const body = { login_token: loginToken, mobile, channel };
|
|
109
113
|
if (appIdentifier) body.app_identifier = appIdentifier;
|
|
110
114
|
return request('POST', '/login/google/mobile/send', body);
|
|
111
115
|
},
|
|
@@ -126,8 +130,8 @@ export function createApi(baseUrl = DEFAULT_BASE, options = {}) {
|
|
|
126
130
|
return request('POST', '/login/google/fr/enroll', fd);
|
|
127
131
|
},
|
|
128
132
|
|
|
129
|
-
async microsoftMobileSend(loginToken, mobile, appIdentifier) {
|
|
130
|
-
const body = { login_token: loginToken, mobile };
|
|
133
|
+
async microsoftMobileSend(loginToken, mobile, appIdentifier, channel = 'sms') {
|
|
134
|
+
const body = { login_token: loginToken, mobile, channel };
|
|
131
135
|
if (appIdentifier) body.app_identifier = appIdentifier;
|
|
132
136
|
return request('POST', '/login/microsoft/mobile/send', body);
|
|
133
137
|
},
|
package/login.js
CHANGED
|
@@ -153,12 +153,14 @@ export function mountLogin(container, options = {}) {
|
|
|
153
153
|
flow.goNext();
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
async function handleIdentifierMobile(mobile) {
|
|
156
|
+
async function handleIdentifierMobile(mobile, channel = 'sms') {
|
|
157
157
|
state.clearError();
|
|
158
158
|
state.set('identifier', mobile);
|
|
159
|
+
state.set('channel', channel);
|
|
159
160
|
if (flow.currentMethod === LOGIN_METHODS.MOBILE_OTP) {
|
|
160
161
|
try {
|
|
161
|
-
await api.otpSend(mobile, '',
|
|
162
|
+
const res = await api.otpSend(mobile, '', channel);
|
|
163
|
+
if (res?.channel) state.set('channel', res.channel);
|
|
162
164
|
flow.goNext();
|
|
163
165
|
} catch (err) {
|
|
164
166
|
const msg = err?.data?.error || err?.message || 'Failed to send code';
|
|
@@ -170,6 +172,16 @@ export function mountLogin(container, options = {}) {
|
|
|
170
172
|
flow.goNext();
|
|
171
173
|
}
|
|
172
174
|
|
|
175
|
+
async function handleOtpResend(channel = 'sms') {
|
|
176
|
+
state.clearError();
|
|
177
|
+
const identifier = state.get('identifier');
|
|
178
|
+
if (!identifier) throw new Error('Identifier missing');
|
|
179
|
+
state.set('channel', channel);
|
|
180
|
+
const res = await api.otpSend(identifier, '', channel);
|
|
181
|
+
if (res?.channel) state.set('channel', res.channel);
|
|
182
|
+
return res;
|
|
183
|
+
}
|
|
184
|
+
|
|
173
185
|
function isFrRequiredError(err) {
|
|
174
186
|
const msg = (err?.data?.error || err?.message || '').toLowerCase();
|
|
175
187
|
return msg.includes('facial recognition') && msg.includes('required');
|
|
@@ -201,11 +213,14 @@ export function mountLogin(container, options = {}) {
|
|
|
201
213
|
}
|
|
202
214
|
}
|
|
203
215
|
|
|
204
|
-
async function handleOtp(otp) {
|
|
216
|
+
async function handleOtp(otp, channelParam = null) {
|
|
205
217
|
state.clearError();
|
|
206
218
|
state.set('otp', otp);
|
|
207
219
|
const identifier = state.get('identifier');
|
|
208
|
-
|
|
220
|
+
let channel = channelParam || state.get('channel');
|
|
221
|
+
if (!channel) {
|
|
222
|
+
channel = identifier.includes('@') ? 'email' : 'sms';
|
|
223
|
+
}
|
|
209
224
|
if (config.frsAsSecondFactor) {
|
|
210
225
|
flow.goNext();
|
|
211
226
|
return;
|
|
@@ -308,6 +323,7 @@ export function mountLogin(container, options = {}) {
|
|
|
308
323
|
onIdentifierMobile: handleIdentifierMobile,
|
|
309
324
|
onPassword: handlePassword,
|
|
310
325
|
onOtp: handleOtp,
|
|
326
|
+
onOtpResend: handleOtpResend,
|
|
311
327
|
onFrs: handleFrs,
|
|
312
328
|
onSuccessContinue,
|
|
313
329
|
onBack: () => { state.clearError(); layout.showError(''); flow.goBack(); },
|
|
@@ -330,6 +346,7 @@ export function mountLogin(container, options = {}) {
|
|
|
330
346
|
onIdentifierMobile: handlers.onIdentifierMobile,
|
|
331
347
|
onPassword: handlePassword,
|
|
332
348
|
onOtp: handleOtp,
|
|
349
|
+
onOtpResend: handlers.onOtpResend,
|
|
333
350
|
onFrs: handleFrs,
|
|
334
351
|
onSuccessContinue: handlers.onSuccessContinue,
|
|
335
352
|
onBack: handlers.onBack,
|
package/package.json
CHANGED
package/signup.js
CHANGED
|
@@ -103,6 +103,9 @@ export function mount(container, options = {}) {
|
|
|
103
103
|
email: all.email,
|
|
104
104
|
mobile: all.mobile,
|
|
105
105
|
userId: all.userId,
|
|
106
|
+
team: all.team,
|
|
107
|
+
subTeam: all.subTeam,
|
|
108
|
+
designation: all.designation,
|
|
106
109
|
};
|
|
107
110
|
if (apiResult && typeof apiResult === 'object' && apiResult !== null) {
|
|
108
111
|
Object.assign(payload, apiResult);
|
|
@@ -141,13 +144,23 @@ export function mount(container, options = {}) {
|
|
|
141
144
|
|
|
142
145
|
async function handleEmailVerify(data) {
|
|
143
146
|
state.clearError();
|
|
144
|
-
state.setMultiple({
|
|
147
|
+
state.setMultiple({
|
|
148
|
+
emailOtp: data.otp,
|
|
149
|
+
firstName: data.firstName,
|
|
150
|
+
lastName: data.lastName,
|
|
151
|
+
team: data.team,
|
|
152
|
+
subTeam: data.subTeam,
|
|
153
|
+
designation: data.designation,
|
|
154
|
+
});
|
|
145
155
|
try {
|
|
146
156
|
const res = await api.signupEmailVerify({
|
|
147
157
|
email: state.get('email'),
|
|
148
158
|
otp: data.otp,
|
|
149
159
|
first_name: data.firstName,
|
|
150
160
|
last_name: data.lastName,
|
|
161
|
+
team: data.team,
|
|
162
|
+
sub_team: data.subTeam,
|
|
163
|
+
designation: data.designation,
|
|
151
164
|
});
|
|
152
165
|
const userId = res?.user_id ?? res?.data?.user_id;
|
|
153
166
|
if (userId) state.set('userId', userId);
|
|
@@ -159,18 +172,21 @@ export function mount(container, options = {}) {
|
|
|
159
172
|
}
|
|
160
173
|
}
|
|
161
174
|
|
|
162
|
-
async function handleMobileSubmit(mobile) {
|
|
175
|
+
async function handleMobileSubmit(mobile, channel = 'sms') {
|
|
163
176
|
state.clearError();
|
|
164
177
|
state.set('mobile', mobile);
|
|
178
|
+
state.set('mobileChannel', channel);
|
|
165
179
|
try {
|
|
180
|
+
let res;
|
|
166
181
|
if (isSocial()) {
|
|
167
182
|
const token = state.get('loginToken');
|
|
168
183
|
const provider = state.get('socialProvider');
|
|
169
|
-
if (provider === 'microsoft') await api.microsoftMobileSend(token, mobile, appIdentifier);
|
|
170
|
-
else await api.googleMobileSend(token, mobile, appIdentifier);
|
|
184
|
+
if (provider === 'microsoft') res = await api.microsoftMobileSend(token, mobile, appIdentifier, channel);
|
|
185
|
+
else res = await api.googleMobileSend(token, mobile, appIdentifier, channel);
|
|
171
186
|
} else {
|
|
172
|
-
await api.signupMobileSendOtp(state.get('email'), mobile);
|
|
187
|
+
res = await api.signupMobileSendOtp(state.get('email'), mobile, channel);
|
|
173
188
|
}
|
|
189
|
+
if (res?.channel) state.set('mobileChannel', res.channel);
|
|
174
190
|
flow.goNext();
|
|
175
191
|
} catch (err) {
|
|
176
192
|
const msg = err?.data?.error || err?.message || 'Failed to send code';
|
|
@@ -179,6 +195,24 @@ export function mount(container, options = {}) {
|
|
|
179
195
|
}
|
|
180
196
|
}
|
|
181
197
|
|
|
198
|
+
async function handleMobileResend(channel = 'sms') {
|
|
199
|
+
state.clearError();
|
|
200
|
+
const mobile = state.get('mobile');
|
|
201
|
+
if (!mobile) throw new Error('Mobile number missing');
|
|
202
|
+
state.set('mobileChannel', channel);
|
|
203
|
+
let res;
|
|
204
|
+
if (isSocial()) {
|
|
205
|
+
const token = state.get('loginToken');
|
|
206
|
+
const provider = state.get('socialProvider');
|
|
207
|
+
if (provider === 'microsoft') res = await api.microsoftMobileSend(token, mobile, appIdentifier, channel);
|
|
208
|
+
else res = await api.googleMobileSend(token, mobile, appIdentifier, channel);
|
|
209
|
+
} else {
|
|
210
|
+
res = await api.signupMobileSendOtp(state.get('email'), mobile, channel);
|
|
211
|
+
}
|
|
212
|
+
if (res?.channel) state.set('mobileChannel', res.channel);
|
|
213
|
+
return res;
|
|
214
|
+
}
|
|
215
|
+
|
|
182
216
|
async function handleMobileVerify(otp) {
|
|
183
217
|
state.clearError();
|
|
184
218
|
state.set('mobileOtp', otp);
|
|
@@ -272,6 +306,7 @@ export function mount(container, options = {}) {
|
|
|
272
306
|
onEmailVerify: handleEmailVerify,
|
|
273
307
|
onMobileSubmit: handleMobileSubmit,
|
|
274
308
|
onMobileVerify: handleMobileVerify,
|
|
309
|
+
onMobileResend: handleMobileResend,
|
|
275
310
|
onPasswordSubmit: handlePasswordSubmit,
|
|
276
311
|
onFaceSubmit: handleFaceSubmit,
|
|
277
312
|
onBack: () => {
|
package/styles.css
CHANGED
|
@@ -1239,3 +1239,99 @@
|
|
|
1239
1239
|
.wf-page-footer { margin-top: 0.75rem; }
|
|
1240
1240
|
.wf-camera-view { aspect-ratio: 16 / 9; }
|
|
1241
1241
|
}
|
|
1242
|
+
|
|
1243
|
+
|
|
1244
|
+
/* ============================= WHATSAPP & CHANNEL SWITCHING ============================= */
|
|
1245
|
+
.wf-btn-whatsapp-outline {
|
|
1246
|
+
border: 1px solid #25D366;
|
|
1247
|
+
background: rgba(37, 211, 102, 0.05);
|
|
1248
|
+
color: #128C7E;
|
|
1249
|
+
font-weight: 600;
|
|
1250
|
+
}
|
|
1251
|
+
.wf-btn-whatsapp-outline:hover:not(:disabled) {
|
|
1252
|
+
background: rgba(37, 211, 102, 0.12);
|
|
1253
|
+
border-color: #25D366;
|
|
1254
|
+
color: #075E54;
|
|
1255
|
+
transform: translateY(-1px);
|
|
1256
|
+
}
|
|
1257
|
+
.wf-icon-box-whatsapp {
|
|
1258
|
+
background: rgba(37, 211, 102, 0.12) !important;
|
|
1259
|
+
color: #25D366 !important;
|
|
1260
|
+
}
|
|
1261
|
+
.wf-channel-alt {
|
|
1262
|
+
margin-top: 0.75rem;
|
|
1263
|
+
display: flex;
|
|
1264
|
+
flex-direction: column;
|
|
1265
|
+
gap: 0.625rem;
|
|
1266
|
+
}
|
|
1267
|
+
.wf-divider-text {
|
|
1268
|
+
display: flex;
|
|
1269
|
+
align-items: center;
|
|
1270
|
+
text-align: center;
|
|
1271
|
+
color: var(--wf-muted);
|
|
1272
|
+
font-size: 0.75rem;
|
|
1273
|
+
}
|
|
1274
|
+
.wf-divider-text::before,
|
|
1275
|
+
.wf-divider-text::after {
|
|
1276
|
+
content: "";
|
|
1277
|
+
flex: 1;
|
|
1278
|
+
border-bottom: 1px solid var(--wf-border);
|
|
1279
|
+
}
|
|
1280
|
+
.wf-divider-text span {
|
|
1281
|
+
padding: 0 0.625rem;
|
|
1282
|
+
text-transform: uppercase;
|
|
1283
|
+
font-size: 0.6875rem;
|
|
1284
|
+
letter-spacing: 0.05em;
|
|
1285
|
+
font-weight: 600;
|
|
1286
|
+
}
|
|
1287
|
+
.wf-whatsapp-link {
|
|
1288
|
+
display: inline-flex;
|
|
1289
|
+
align-items: center;
|
|
1290
|
+
gap: 0.375rem;
|
|
1291
|
+
color: #128C7E;
|
|
1292
|
+
font-weight: 600;
|
|
1293
|
+
font-size: 0.8125rem;
|
|
1294
|
+
background: none;
|
|
1295
|
+
border: none;
|
|
1296
|
+
cursor: pointer;
|
|
1297
|
+
padding: 0.25rem 0.5rem;
|
|
1298
|
+
border-radius: var(--wf-radius);
|
|
1299
|
+
transition: all 0.15s var(--wf-ease);
|
|
1300
|
+
}
|
|
1301
|
+
.wf-whatsapp-link:hover {
|
|
1302
|
+
background: rgba(37, 211, 102, 0.12);
|
|
1303
|
+
color: #075E54;
|
|
1304
|
+
}
|
|
1305
|
+
.wf-verify-footer {
|
|
1306
|
+
margin-top: 1rem;
|
|
1307
|
+
display: flex;
|
|
1308
|
+
flex-direction: column;
|
|
1309
|
+
align-items: center;
|
|
1310
|
+
gap: 0.5rem;
|
|
1311
|
+
text-align: center;
|
|
1312
|
+
}
|
|
1313
|
+
.wf-channel-switch-wrap {
|
|
1314
|
+
display: flex;
|
|
1315
|
+
align-items: center;
|
|
1316
|
+
justify-content: center;
|
|
1317
|
+
}
|
|
1318
|
+
.wf-status-message {
|
|
1319
|
+
font-size: 0.8125rem;
|
|
1320
|
+
font-weight: 500;
|
|
1321
|
+
padding: 0.375rem 0.75rem;
|
|
1322
|
+
border-radius: var(--wf-radius-sm);
|
|
1323
|
+
transition: all 0.2s ease;
|
|
1324
|
+
width: 100%;
|
|
1325
|
+
}
|
|
1326
|
+
.wf-status-message.success {
|
|
1327
|
+
background: var(--wf-success-bg);
|
|
1328
|
+
color: var(--wf-success);
|
|
1329
|
+
}
|
|
1330
|
+
.wf-status-message.error {
|
|
1331
|
+
background: var(--wf-destructive-bg);
|
|
1332
|
+
color: var(--wf-destructive);
|
|
1333
|
+
}
|
|
1334
|
+
.wf-status-message.info {
|
|
1335
|
+
background: var(--wf-primary-light);
|
|
1336
|
+
color: var(--wf-primary);
|
|
1337
|
+
}
|
|
@@ -38,6 +38,20 @@ export function createEmailVerifyScreen(email, onNext, onBack, prefill = {}) {
|
|
|
38
38
|
<input type="text" id="wf-last-name" placeholder="Doe" class="wf-input" required autocomplete="family-name" />
|
|
39
39
|
</div>
|
|
40
40
|
</div>
|
|
41
|
+
<div class="wf-row-2">
|
|
42
|
+
<div class="wf-field">
|
|
43
|
+
<label class="wf-label" for="wf-team">Team</label>
|
|
44
|
+
<input type="text" id="wf-team" placeholder="Engineering" class="wf-input" autocomplete="organization" />
|
|
45
|
+
</div>
|
|
46
|
+
<div class="wf-field">
|
|
47
|
+
<label class="wf-label" for="wf-sub-team">Sub-team</label>
|
|
48
|
+
<input type="text" id="wf-sub-team" placeholder="Backend" class="wf-input" />
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
<div class="wf-field">
|
|
52
|
+
<label class="wf-label" for="wf-designation">Designation</label>
|
|
53
|
+
<input type="text" id="wf-designation" placeholder="Software Engineer" class="wf-input" autocomplete="organization-title" />
|
|
54
|
+
</div>
|
|
41
55
|
<div class="wf-btn-row">
|
|
42
56
|
<button type="button" class="wf-btn wf-btn-outline wf-btn-lg" data-action="back" aria-label="Go back">
|
|
43
57
|
<svg class="wf-icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${ARROW_LEFT}"/></svg>
|
|
@@ -57,9 +71,15 @@ export function createEmailVerifyScreen(email, onNext, onBack, prefill = {}) {
|
|
|
57
71
|
const otpEls = wrap.querySelectorAll('.wf-otp-input');
|
|
58
72
|
const firstName = wrap.querySelector('#wf-first-name');
|
|
59
73
|
const lastName = wrap.querySelector('#wf-last-name');
|
|
74
|
+
const team = wrap.querySelector('#wf-team');
|
|
75
|
+
const subTeam = wrap.querySelector('#wf-sub-team');
|
|
76
|
+
const designation = wrap.querySelector('#wf-designation');
|
|
60
77
|
|
|
61
78
|
if (prefill.firstName) firstName.value = prefill.firstName;
|
|
62
79
|
if (prefill.lastName) lastName.value = prefill.lastName;
|
|
80
|
+
if (prefill.team && team) team.value = prefill.team;
|
|
81
|
+
if (prefill.subTeam && subTeam) subTeam.value = prefill.subTeam;
|
|
82
|
+
if (prefill.designation && designation) designation.value = prefill.designation;
|
|
63
83
|
|
|
64
84
|
function handleOtpPaste(e) {
|
|
65
85
|
e.preventDefault();
|
|
@@ -86,7 +106,14 @@ export function createEmailVerifyScreen(email, onNext, onBack, prefill = {}) {
|
|
|
86
106
|
e.preventDefault();
|
|
87
107
|
const code = [...otpEls].map((o) => o.value).join('');
|
|
88
108
|
if (code.length === 6 && firstName.value.trim() && lastName.value.trim()) {
|
|
89
|
-
onNext({
|
|
109
|
+
onNext({
|
|
110
|
+
otp: code,
|
|
111
|
+
firstName: firstName.value.trim(),
|
|
112
|
+
lastName: lastName.value.trim(),
|
|
113
|
+
team: team ? team.value.trim() : '',
|
|
114
|
+
subTeam: subTeam ? subTeam.value.trim() : '',
|
|
115
|
+
designation: designation ? designation.value.trim() : '',
|
|
116
|
+
});
|
|
90
117
|
}
|
|
91
118
|
});
|
|
92
119
|
|
package/ui/screens/index.js
CHANGED
|
@@ -18,6 +18,7 @@ export function createScreen(step, state, flow, callbacks) {
|
|
|
18
18
|
onEmailVerify,
|
|
19
19
|
onMobileSubmit,
|
|
20
20
|
onMobileVerify,
|
|
21
|
+
onMobileResend,
|
|
21
22
|
onPasswordSubmit,
|
|
22
23
|
onFaceSubmit,
|
|
23
24
|
onBack,
|
|
@@ -37,7 +38,13 @@ export function createScreen(step, state, flow, callbacks) {
|
|
|
37
38
|
state.get('email'),
|
|
38
39
|
(data) => onEmailVerify(data),
|
|
39
40
|
onBack,
|
|
40
|
-
{
|
|
41
|
+
{
|
|
42
|
+
firstName: state.get('firstName') || '',
|
|
43
|
+
lastName: state.get('lastName') || '',
|
|
44
|
+
team: state.get('team') || '',
|
|
45
|
+
subTeam: state.get('subTeam') || '',
|
|
46
|
+
designation: state.get('designation') || '',
|
|
47
|
+
}
|
|
41
48
|
);
|
|
42
49
|
|
|
43
50
|
case STEPS.MOBILE:
|
|
@@ -46,8 +53,12 @@ export function createScreen(step, state, flow, callbacks) {
|
|
|
46
53
|
case STEPS.MOBILE_VERIFY:
|
|
47
54
|
return createMobileVerifyScreen(
|
|
48
55
|
state.get('mobile'),
|
|
49
|
-
(otp) => onMobileVerify(otp),
|
|
50
|
-
onBack
|
|
56
|
+
(otp, channel) => onMobileVerify(otp, channel),
|
|
57
|
+
onBack,
|
|
58
|
+
{
|
|
59
|
+
channel: state.get('mobileChannel') || 'sms',
|
|
60
|
+
onResend: onMobileResend,
|
|
61
|
+
}
|
|
51
62
|
);
|
|
52
63
|
|
|
53
64
|
case STEPS.PASSWORD:
|
|
@@ -1,34 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Login — Mobile identifier input (country code + number)
|
|
3
|
-
* v2.0 — warm, modern design
|
|
3
|
+
* v2.0 — warm, modern design with SMS and WhatsApp support
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
const PHONE_ICON =
|
|
7
|
-
const ARROW_LEFT =
|
|
8
|
-
const ARROW_RIGHT =
|
|
6
|
+
const PHONE_ICON = "M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07 19.5 19.5 0 01-6-6 19.79 19.79 0 01-3.07-8.67A2 2 0 014.11 2h3a2 2 0 012 1.72 12.84 12.84 0 00.7 2.81 2 2 0 0102.78 12.84 12.84 0 006 6 2 2 0 002.78 0 12.84 12.84 0 002.81-.7A2 2 0 0022 16.92z";
|
|
7
|
+
const ARROW_LEFT = "M19 12H5m7 7l-7-7 7-7";
|
|
8
|
+
const ARROW_RIGHT = "M5 12h14m-7-7l7 7-7 7";
|
|
9
|
+
const WHATSAPP_ICON = "M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51l-.57-.01c-.198 0-.52.074-.792.372s-1.04 1.016-1.04 2.479 1.065 2.876 1.213 3.074c.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.888 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L0 24l6.335-1.662c1.746.953 3.71 1.455 5.711 1.456h.005c6.554 0 11.89-5.336 11.893-11.893a11.821 11.821 0 00-3.48-8.413z";
|
|
9
10
|
|
|
10
11
|
const COUNTRY_CODES = [
|
|
11
|
-
{ code:
|
|
12
|
-
{ code:
|
|
13
|
-
{ code:
|
|
14
|
-
{ code:
|
|
15
|
-
{ code:
|
|
16
|
-
{ code:
|
|
17
|
-
{ code:
|
|
18
|
-
{ code:
|
|
19
|
-
{ code:
|
|
20
|
-
{ code:
|
|
21
|
-
{ code:
|
|
22
|
-
{ code:
|
|
12
|
+
{ code: "91", label: "India +91" },
|
|
13
|
+
{ code: "1", label: "United States +1" },
|
|
14
|
+
{ code: "44", label: "United Kingdom +44" },
|
|
15
|
+
{ code: "61", label: "Australia +61" },
|
|
16
|
+
{ code: "81", label: "Japan +81" },
|
|
17
|
+
{ code: "49", label: "Germany +49" },
|
|
18
|
+
{ code: "33", label: "France +33" },
|
|
19
|
+
{ code: "86", label: "China +86" },
|
|
20
|
+
{ code: "971", label: "UAE +971" },
|
|
21
|
+
{ code: "65", label: "Singapore +65" },
|
|
22
|
+
{ code: "55", label: "Brazil +55" },
|
|
23
|
+
{ code: "52", label: "Mexico +52" },
|
|
23
24
|
];
|
|
24
25
|
|
|
25
26
|
export function createLoginIdentifierMobileScreen(onNext, onBack, onSignUp) {
|
|
26
|
-
const wrap = document.createElement(
|
|
27
|
-
wrap.className =
|
|
27
|
+
const wrap = document.createElement("div");
|
|
28
|
+
wrap.className = "wf-screen";
|
|
28
29
|
|
|
29
30
|
const countryOptions = COUNTRY_CODES.map((c) =>
|
|
30
|
-
`<option value="${c.code}"${c.code ===
|
|
31
|
-
).join(
|
|
31
|
+
`<option value="${c.code}"${c.code === "91" ? " selected" : ""}>${c.label}</option>`
|
|
32
|
+
).join("");
|
|
32
33
|
|
|
33
34
|
wrap.innerHTML = `
|
|
34
35
|
<div class="wf-screen-header">
|
|
@@ -36,7 +37,7 @@ export function createLoginIdentifierMobileScreen(onNext, onBack, onSignUp) {
|
|
|
36
37
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${PHONE_ICON}"/></svg>
|
|
37
38
|
</div>
|
|
38
39
|
<h2 class="wf-screen-title">Enter your mobile number</h2>
|
|
39
|
-
<p class="wf-screen-desc">We
|
|
40
|
+
<p class="wf-screen-desc">We will use this to sign you in</p>
|
|
40
41
|
</div>
|
|
41
42
|
<form class="wf-form" id="wf-login-mobile-form">
|
|
42
43
|
<div class="wf-field">
|
|
@@ -52,33 +53,58 @@ export function createLoginIdentifierMobileScreen(onNext, onBack, onSignUp) {
|
|
|
52
53
|
<button type="button" class="wf-btn wf-btn-outline wf-btn-lg" data-action="back" aria-label="Go back">
|
|
53
54
|
<svg class="wf-icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${ARROW_LEFT}"/></svg>
|
|
54
55
|
</button>
|
|
55
|
-
<button type="submit" class="wf-btn wf-btn-primary wf-btn-lg wf-btn-flex">
|
|
56
|
+
<button type="submit" class="wf-btn wf-btn-primary wf-btn-lg wf-btn-flex" id="wf-btn-login-submit">
|
|
56
57
|
Continue
|
|
57
58
|
<svg class="wf-icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${ARROW_RIGHT}"/></svg>
|
|
58
59
|
</button>
|
|
59
60
|
</div>
|
|
61
|
+
<div class="wf-channel-alt">
|
|
62
|
+
<div class="wf-divider-text"><span>or</span></div>
|
|
63
|
+
<button type="button" class="wf-btn wf-btn-outline wf-btn-whatsapp-outline wf-btn-lg wf-btn-full" id="wf-btn-login-wa">
|
|
64
|
+
<svg class="wf-icon-sm" viewBox="0 0 24 24" fill="currentColor"><path d="${WHATSAPP_ICON}"/></svg>
|
|
65
|
+
Send WhatsApp OTP instead
|
|
66
|
+
</button>
|
|
67
|
+
</div>
|
|
60
68
|
</form>
|
|
61
69
|
<p class="wf-footer-link">
|
|
62
70
|
Don't have an account? <button type="button" class="wf-link" data-action="signup">Sign up</button>
|
|
63
71
|
</p>
|
|
64
72
|
`;
|
|
65
73
|
|
|
66
|
-
const form = wrap.querySelector(
|
|
67
|
-
const countrySelect = wrap.querySelector(
|
|
68
|
-
const input = wrap.querySelector(
|
|
74
|
+
const form = wrap.querySelector("#wf-login-mobile-form");
|
|
75
|
+
const countrySelect = wrap.querySelector("#wf-login-mobile-country");
|
|
76
|
+
const input = wrap.querySelector("#wf-login-mobile");
|
|
77
|
+
const waBtn = wrap.querySelector("#wf-btn-login-wa");
|
|
69
78
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const digits = input.value.trim().replace(/\D/g, '');
|
|
79
|
+
function submitMobile(channel = "sms") {
|
|
80
|
+
const digits = input.value.trim().replace(/\D/g, "");
|
|
73
81
|
if (digits.length >= 10) {
|
|
74
|
-
const full =
|
|
75
|
-
onNext(full);
|
|
82
|
+
const full = "+" + countrySelect.value + digits;
|
|
83
|
+
onNext(full, channel);
|
|
84
|
+
} else {
|
|
85
|
+
input.focus();
|
|
86
|
+
if (typeof input.reportValidity === "function") {
|
|
87
|
+
input.reportValidity();
|
|
88
|
+
}
|
|
76
89
|
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
form.addEventListener("submit", (e) => {
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
submitMobile("sms");
|
|
77
95
|
});
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
96
|
+
|
|
97
|
+
if (waBtn) {
|
|
98
|
+
waBtn.addEventListener("click", (e) => {
|
|
99
|
+
e.preventDefault();
|
|
100
|
+
submitMobile("whatsapp");
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
wrap.querySelector("button[data-action='back']").addEventListener("click", onBack);
|
|
105
|
+
const signUpBtn = wrap.querySelector("button[data-action='signup']");
|
|
106
|
+
if (signUpBtn && typeof onSignUp === "function") {
|
|
107
|
+
signUpBtn.addEventListener("click", () => onSignUp());
|
|
82
108
|
}
|
|
83
109
|
|
|
84
110
|
return wrap;
|
|
@@ -19,6 +19,7 @@ export function createLoginScreen(stepId, flow, state, config, handlers) {
|
|
|
19
19
|
onIdentifierMobile,
|
|
20
20
|
onPassword,
|
|
21
21
|
onOtp,
|
|
22
|
+
onOtpResend,
|
|
22
23
|
onFrs,
|
|
23
24
|
onSuccessContinue,
|
|
24
25
|
onBack,
|
|
@@ -75,13 +76,17 @@ export function createLoginScreen(stepId, flow, state, config, handlers) {
|
|
|
75
76
|
|
|
76
77
|
case LOGIN_STEPS.OTP: {
|
|
77
78
|
const identifier = state.get('identifier') || '';
|
|
78
|
-
const
|
|
79
|
-
const otpLength = (
|
|
79
|
+
const currentChannel = state.get('channel') || (identifier.includes('@') ? 'email' : 'sms');
|
|
80
|
+
const otpLength = (currentChannel === 'whatsapp' || identifier.includes('@')) ? 6 : 4;
|
|
80
81
|
return createLoginOtpScreen(
|
|
81
82
|
identifier,
|
|
82
83
|
otpLength,
|
|
83
|
-
(otp) => { state.set('otp', otp); onOtp(otp); },
|
|
84
|
-
onBack
|
|
84
|
+
(otp, chan) => { state.set('otp', otp); onOtp(otp, chan); },
|
|
85
|
+
onBack,
|
|
86
|
+
{
|
|
87
|
+
channel: currentChannel,
|
|
88
|
+
onResend: onOtpResend,
|
|
89
|
+
}
|
|
85
90
|
);
|
|
86
91
|
}
|
|
87
92
|
|
package/ui/screens/login/otp.js
CHANGED
|
@@ -1,79 +1,212 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Login — OTP verification (email or mobile)
|
|
3
|
-
* v2.0 — warm, modern design
|
|
3
|
+
* v2.0 — warm, modern design with SMS and WhatsApp channel switching
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
const MAIL_ICON =
|
|
7
|
-
const
|
|
8
|
-
const
|
|
6
|
+
const MAIL_ICON = "M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z";
|
|
7
|
+
const PHONE_ICON = "M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z";
|
|
8
|
+
const WHATSAPP_ICON = "M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51l-.57-.01c-.198 0-.52.074-.792.372s-1.04 1.016-1.04 2.479 1.065 2.876 1.213 3.074c.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.888 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L0 24l6.335-1.662c1.746.953 3.71 1.455 5.711 1.456h.005c6.554 0 11.89-5.336 11.893-11.893a11.821 11.821 0 00-3.48-8.413z";
|
|
9
|
+
const ARROW_LEFT = "M19 12H5m7 7l-7-7 7-7";
|
|
10
|
+
const ARROW_RIGHT = "M5 12h14m-7-7l7 7-7 7";
|
|
9
11
|
|
|
10
|
-
export function createLoginOtpScreen(identifier, otpLength = 4, onNext, onBack) {
|
|
11
|
-
const
|
|
12
|
-
|
|
12
|
+
export function createLoginOtpScreen(identifier, otpLength = 4, onNext, onBack, options = {}) {
|
|
13
|
+
const isEmail = identifier.includes("@");
|
|
14
|
+
let currentChannel = (options.channel || (isEmail ? "email" : "sms")).toLowerCase();
|
|
15
|
+
const onResend = typeof options.onResend === "function" ? options.onResend : null;
|
|
13
16
|
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
`).join('');
|
|
17
|
+
const wrap = document.createElement("div");
|
|
18
|
+
wrap.className = "wf-screen";
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
20
|
+
let cooldownTimer = null;
|
|
21
|
+
let cooldownSec = 0;
|
|
22
|
+
|
|
23
|
+
function getExpectedLength() {
|
|
24
|
+
if (isEmail) return 6;
|
|
25
|
+
return currentChannel === "whatsapp" ? 6 : 4;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getDescText(len) {
|
|
29
|
+
if (isEmail) {
|
|
30
|
+
return `We sent a ${len}-digit code to <span class="wf-highlight">${identifier}</span>`;
|
|
31
|
+
}
|
|
32
|
+
if (currentChannel === "whatsapp") {
|
|
33
|
+
return `We sent a ${len}-digit code via WhatsApp to <span class="wf-highlight">${identifier}</span>`;
|
|
34
|
+
}
|
|
35
|
+
return `We sent a ${len}-digit code to <span class="wf-highlight">${identifier}</span>`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function renderScreen() {
|
|
39
|
+
const len = getExpectedLength();
|
|
40
|
+
const isWhatsApp = !isEmail && currentChannel === "whatsapp";
|
|
41
|
+
|
|
42
|
+
wrap.innerHTML = `
|
|
43
|
+
<div class="wf-screen-header">
|
|
44
|
+
<div class="wf-icon-box ${isWhatsApp ? "wf-icon-box-whatsapp" : ""}">
|
|
45
|
+
${isEmail
|
|
46
|
+
? `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${MAIL_ICON}"/></svg>`
|
|
47
|
+
: isWhatsApp
|
|
48
|
+
? `<svg viewBox="0 0 24 24" fill="currentColor"><path d="${WHATSAPP_ICON}"/></svg>`
|
|
49
|
+
: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${PHONE_ICON}"/></svg>`
|
|
50
|
+
}
|
|
51
|
+
</div>
|
|
52
|
+
<h2 class="wf-screen-title">Check your code</h2>
|
|
53
|
+
<p class="wf-screen-desc" id="wf-login-otp-desc">${getDescText(len)}</p>
|
|
30
54
|
</div>
|
|
31
|
-
<
|
|
32
|
-
<
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
55
|
+
<form class="wf-form" id="wf-login-otp-form">
|
|
56
|
+
<div class="wf-field">
|
|
57
|
+
<label class="wf-label">Verification code</label>
|
|
58
|
+
<div class="wf-otp-row" id="wf-login-otp-container">
|
|
59
|
+
${Array(len).fill(0).map((_, i) => `
|
|
60
|
+
<input type="text" inputmode="numeric" maxlength="1" class="wf-otp-input" id="wf-login-otp-${i}" data-idx="${i}" autocomplete="one-time-code" />
|
|
61
|
+
`).join("")}
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
<div class="wf-btn-row">
|
|
65
|
+
<button type="button" class="wf-btn wf-btn-outline wf-btn-lg" data-action="back" aria-label="Go back">
|
|
66
|
+
<svg class="wf-icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${ARROW_LEFT}"/></svg>
|
|
67
|
+
</button>
|
|
68
|
+
<button type="submit" class="wf-btn wf-btn-primary wf-btn-lg wf-btn-flex">
|
|
69
|
+
Verify
|
|
70
|
+
<svg class="wf-icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${ARROW_RIGHT}"/></svg>
|
|
71
|
+
</button>
|
|
72
|
+
</div>
|
|
73
|
+
</form>
|
|
74
|
+
<div class="wf-verify-footer">
|
|
75
|
+
<p class="wf-footer-link">
|
|
76
|
+
Didn't receive the code?
|
|
77
|
+
<button type="button" class="wf-link" id="wf-btn-login-resend">
|
|
78
|
+
${cooldownSec > 0 ? `Resend (${cooldownSec}s)` : (isWhatsApp ? "Resend WhatsApp OTP" : "Resend")}
|
|
79
|
+
</button>
|
|
80
|
+
</p>
|
|
81
|
+
${!isEmail ? `
|
|
82
|
+
<div class="wf-channel-switch-wrap">
|
|
83
|
+
${isWhatsApp
|
|
84
|
+
? `<button type="button" class="wf-link wf-channel-switch-btn" id="wf-btn-login-switch">Send via SMS instead</button>`
|
|
85
|
+
: `<button type="button" class="wf-whatsapp-link wf-channel-switch-btn" id="wf-btn-login-switch">
|
|
86
|
+
<svg class="wf-icon-sm" viewBox="0 0 24 24" fill="currentColor"><path d="${WHATSAPP_ICON}"/></svg>
|
|
87
|
+
Send WhatsApp OTP instead
|
|
88
|
+
</button>`
|
|
89
|
+
}
|
|
90
|
+
</div>
|
|
91
|
+
` : ""}
|
|
92
|
+
<div id="wf-login-resend-status" class="wf-status-message" style="display:none;"></div>
|
|
39
93
|
</div>
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const nextIdx = Math.min(digits.length, otpLength - 1);
|
|
55
|
-
if (otpEls[nextIdx]) otpEls[nextIdx].focus();
|
|
94
|
+
`;
|
|
95
|
+
|
|
96
|
+
bindEvents();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function setStatus(msg, type = "success") {
|
|
100
|
+
const el = wrap.querySelector("#wf-login-resend-status");
|
|
101
|
+
if (!el) return;
|
|
102
|
+
el.textContent = msg;
|
|
103
|
+
el.className = `wf-status-message ${type}`;
|
|
104
|
+
el.style.display = "block";
|
|
105
|
+
setTimeout(() => {
|
|
106
|
+
if (el) el.style.display = "none";
|
|
107
|
+
}, 4500);
|
|
56
108
|
}
|
|
57
109
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
110
|
+
function startCooldown() {
|
|
111
|
+
cooldownSec = 30;
|
|
112
|
+
const resendBtn = wrap.querySelector("#wf-btn-login-resend");
|
|
113
|
+
if (resendBtn) {
|
|
114
|
+
resendBtn.disabled = true;
|
|
115
|
+
resendBtn.textContent = `Resend (${cooldownSec}s)`;
|
|
116
|
+
}
|
|
117
|
+
if (cooldownTimer) clearInterval(cooldownTimer);
|
|
118
|
+
cooldownTimer = setInterval(() => {
|
|
119
|
+
cooldownSec -= 1;
|
|
120
|
+
const btn = wrap.querySelector("#wf-btn-login-resend");
|
|
121
|
+
if (cooldownSec <= 0) {
|
|
122
|
+
clearInterval(cooldownTimer);
|
|
123
|
+
cooldownTimer = null;
|
|
124
|
+
if (btn) {
|
|
125
|
+
btn.disabled = false;
|
|
126
|
+
btn.textContent = currentChannel === "whatsapp" ? "Resend WhatsApp OTP" : "Resend";
|
|
127
|
+
}
|
|
128
|
+
} else if (btn) {
|
|
129
|
+
btn.textContent = `Resend (${cooldownSec}s)`;
|
|
130
|
+
}
|
|
131
|
+
}, 1000);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function triggerResend(targetChannel) {
|
|
135
|
+
if (!onResend) return;
|
|
136
|
+
try {
|
|
137
|
+
const isSwitch = targetChannel !== currentChannel;
|
|
138
|
+
currentChannel = targetChannel;
|
|
139
|
+
if (isSwitch) {
|
|
140
|
+
renderScreen();
|
|
141
|
+
}
|
|
142
|
+
setStatus("Sending verification code...", "info");
|
|
143
|
+
const res = await onResend(targetChannel);
|
|
144
|
+
const chanLabel = targetChannel === "whatsapp" ? "WhatsApp" : (targetChannel === "email" ? "Email" : "SMS");
|
|
145
|
+
setStatus(res?.message || `Code sent via ${chanLabel}!`, "success");
|
|
146
|
+
startCooldown();
|
|
147
|
+
const firstOtp = wrap.querySelector("#wf-login-otp-0");
|
|
148
|
+
if (firstOtp) firstOtp.focus();
|
|
149
|
+
} catch (err) {
|
|
150
|
+
const msg = err?.data?.error || err?.message || "Failed to resend code";
|
|
151
|
+
setStatus(msg, "error");
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function bindEvents() {
|
|
156
|
+
const form = wrap.querySelector("#wf-login-otp-form");
|
|
157
|
+
const otpEls = wrap.querySelectorAll(".wf-otp-input");
|
|
158
|
+
const len = getExpectedLength();
|
|
159
|
+
|
|
160
|
+
function handleOtpPaste(e) {
|
|
161
|
+
e.preventDefault();
|
|
162
|
+
const pasted = (e.clipboardData || window.clipboardData).getData("text");
|
|
163
|
+
const digits = pasted.replace(/\D/g, "").slice(0, len).split("");
|
|
164
|
+
digits.forEach((d, i) => { if (otpEls[i]) otpEls[i].value = d; });
|
|
165
|
+
const nextIdx = Math.min(digits.length, len - 1);
|
|
166
|
+
if (otpEls[nextIdx]) otpEls[nextIdx].focus();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
otpEls.forEach((el, i) => {
|
|
170
|
+
el.addEventListener("paste", handleOtpPaste);
|
|
171
|
+
el.addEventListener("input", (e) => {
|
|
172
|
+
const v = e.target.value.replace(/\D/g, "").slice(0, 1);
|
|
173
|
+
e.target.value = v;
|
|
174
|
+
if (v && i < len - 1) otpEls[i + 1].focus();
|
|
175
|
+
});
|
|
176
|
+
el.addEventListener("keydown", (e) => {
|
|
177
|
+
if (e.key === "Backspace" && !e.target.value && i > 0) otpEls[i - 1].focus();
|
|
178
|
+
});
|
|
64
179
|
});
|
|
65
|
-
|
|
66
|
-
|
|
180
|
+
|
|
181
|
+
form.addEventListener("submit", (e) => {
|
|
182
|
+
e.preventDefault();
|
|
183
|
+
const code = [...otpEls].map((o) => o.value).join("");
|
|
184
|
+
if (code.length === len || (code.length >= 4 && code.length <= 6)) {
|
|
185
|
+
onNext(code, currentChannel);
|
|
186
|
+
}
|
|
67
187
|
});
|
|
68
|
-
});
|
|
69
188
|
|
|
70
|
-
|
|
71
|
-
e.preventDefault();
|
|
72
|
-
const code = [...otpEls].map((o) => o.value).join('');
|
|
73
|
-
if (code.length === otpLength) onNext(code);
|
|
74
|
-
});
|
|
189
|
+
wrap.querySelector("button[data-action='back']").addEventListener("click", onBack);
|
|
75
190
|
|
|
76
|
-
|
|
191
|
+
const resendBtn = wrap.querySelector("#wf-btn-login-resend");
|
|
192
|
+
if (resendBtn) {
|
|
193
|
+
if (cooldownSec > 0) resendBtn.disabled = true;
|
|
194
|
+
resendBtn.addEventListener("click", (e) => {
|
|
195
|
+
e.preventDefault();
|
|
196
|
+
if (cooldownSec <= 0) triggerResend(currentChannel);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const switchBtn = wrap.querySelector("#wf-btn-login-switch");
|
|
201
|
+
if (switchBtn) {
|
|
202
|
+
switchBtn.addEventListener("click", (e) => {
|
|
203
|
+
e.preventDefault();
|
|
204
|
+
const nextChannel = currentChannel === "whatsapp" ? "sms" : "whatsapp";
|
|
205
|
+
triggerResend(nextChannel);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
77
209
|
|
|
210
|
+
renderScreen();
|
|
78
211
|
return wrap;
|
|
79
212
|
}
|
|
@@ -1,34 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Mobile Entry Screen — Add mobile number (country code + number)
|
|
3
|
-
* v2.0 — warm, modern design
|
|
3
|
+
* v2.0 — warm, modern design with SMS and WhatsApp OTP choices
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
const PHONE_ICON =
|
|
7
|
-
const ARROW_LEFT =
|
|
8
|
-
const ARROW_RIGHT =
|
|
6
|
+
const PHONE_ICON = "M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07 19.5 19.5 0 01-6-6 19.79 19.79 0 01-3.07-8.67A2 2 0 014.11 2h3a2 2 0 012 1.72 12.84 12.84 0 00.7 2.81 2 2 0 0102.78 12.84 12.84 0 006 6 2 2 0 002.78 0 12.84 12.84 0 002.81-.7A2 2 0 0022 16.92z";
|
|
7
|
+
const ARROW_LEFT = "M19 12H5m7 7l-7-7 7-7";
|
|
8
|
+
const ARROW_RIGHT = "M5 12h14m-7-7l7 7-7 7";
|
|
9
|
+
const WHATSAPP_ICON = "M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51l-.57-.01c-.198 0-.52.074-.792.372s-1.04 1.016-1.04 2.479 1.065 2.876 1.213 3.074c.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.888 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L0 24l6.335-1.662c1.746.953 3.71 1.455 5.711 1.456h.005c6.554 0 11.89-5.336 11.893-11.893a11.821 11.821 0 00-3.48-8.413z";
|
|
9
10
|
|
|
10
11
|
const COUNTRY_CODES = [
|
|
11
|
-
{ code:
|
|
12
|
-
{ code:
|
|
13
|
-
{ code:
|
|
14
|
-
{ code:
|
|
15
|
-
{ code:
|
|
16
|
-
{ code:
|
|
17
|
-
{ code:
|
|
18
|
-
{ code:
|
|
19
|
-
{ code:
|
|
20
|
-
{ code:
|
|
21
|
-
{ code:
|
|
22
|
-
{ code:
|
|
12
|
+
{ code: "91", label: "+91" },
|
|
13
|
+
{ code: "1", label: "+1" },
|
|
14
|
+
{ code: "44", label: "+44" },
|
|
15
|
+
{ code: "61", label: "+61" },
|
|
16
|
+
{ code: "81", label: "+81" },
|
|
17
|
+
{ code: "49", label: "+49" },
|
|
18
|
+
{ code: "33", label: "+33" },
|
|
19
|
+
{ code: "86", label: "+86" },
|
|
20
|
+
{ code: "971", label: "+971" },
|
|
21
|
+
{ code: "65", label: "+65" },
|
|
22
|
+
{ code: "55", label: "+55" },
|
|
23
|
+
{ code: "52", label: "+52" },
|
|
23
24
|
];
|
|
24
25
|
|
|
25
26
|
export function createMobileEntryScreen(onNext, onBack) {
|
|
26
|
-
const wrap = document.createElement(
|
|
27
|
-
wrap.className =
|
|
27
|
+
const wrap = document.createElement("div");
|
|
28
|
+
wrap.className = "wf-screen";
|
|
28
29
|
|
|
29
30
|
const countryOptions = COUNTRY_CODES.map((c) =>
|
|
30
|
-
`<option value="${c.code}"${c.code ===
|
|
31
|
-
).join(
|
|
31
|
+
`<option value="${c.code}"${c.code === "91" ? " selected" : ""}>${c.label}</option>`
|
|
32
|
+
).join("");
|
|
32
33
|
|
|
33
34
|
wrap.innerHTML = `
|
|
34
35
|
<div class="wf-screen-header">
|
|
@@ -36,7 +37,7 @@ export function createMobileEntryScreen(onNext, onBack) {
|
|
|
36
37
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${PHONE_ICON}"/></svg>
|
|
37
38
|
</div>
|
|
38
39
|
<h2 class="wf-screen-title">Add your mobile number</h2>
|
|
39
|
-
<p class="wf-screen-desc">We
|
|
40
|
+
<p class="wf-screen-desc">We will send a verification code to confirm this number</p>
|
|
40
41
|
</div>
|
|
41
42
|
<form class="wf-form" id="wf-mobile-form">
|
|
42
43
|
<div class="wf-field">
|
|
@@ -52,28 +53,52 @@ export function createMobileEntryScreen(onNext, onBack) {
|
|
|
52
53
|
<button type="button" class="wf-btn wf-btn-outline wf-btn-lg" data-action="back" aria-label="Go back">
|
|
53
54
|
<svg class="wf-icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${ARROW_LEFT}"/></svg>
|
|
54
55
|
</button>
|
|
55
|
-
<button type="submit" class="wf-btn wf-btn-primary wf-btn-lg wf-btn-flex">
|
|
56
|
+
<button type="submit" class="wf-btn wf-btn-primary wf-btn-lg wf-btn-flex" id="wf-btn-send-sms">
|
|
56
57
|
Send Code
|
|
57
58
|
<svg class="wf-icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${ARROW_RIGHT}"/></svg>
|
|
58
59
|
</button>
|
|
59
60
|
</div>
|
|
61
|
+
<div class="wf-channel-alt">
|
|
62
|
+
<div class="wf-divider-text"><span>or</span></div>
|
|
63
|
+
<button type="button" class="wf-btn wf-btn-outline wf-btn-whatsapp-outline wf-btn-lg wf-btn-full" id="wf-btn-send-wa">
|
|
64
|
+
<svg class="wf-icon-sm" viewBox="0 0 24 24" fill="currentColor"><path d="${WHATSAPP_ICON}"/></svg>
|
|
65
|
+
Send WhatsApp OTP instead
|
|
66
|
+
</button>
|
|
67
|
+
</div>
|
|
60
68
|
</form>
|
|
61
69
|
`;
|
|
62
70
|
|
|
63
|
-
const form = wrap.querySelector(
|
|
64
|
-
const countrySelect = wrap.querySelector(
|
|
65
|
-
const input = wrap.querySelector(
|
|
71
|
+
const form = wrap.querySelector("#wf-mobile-form");
|
|
72
|
+
const countrySelect = wrap.querySelector("#wf-mobile-country");
|
|
73
|
+
const input = wrap.querySelector("#wf-mobile");
|
|
74
|
+
const waBtn = wrap.querySelector("#wf-btn-send-wa");
|
|
66
75
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const digits = input.value.trim().replace(/\D/g, '');
|
|
76
|
+
function submitMobile(channel = "sms") {
|
|
77
|
+
const digits = input.value.trim().replace(/\D/g, "");
|
|
70
78
|
if (digits.length >= 10) {
|
|
71
|
-
const full =
|
|
72
|
-
onNext(full);
|
|
79
|
+
const full = "+" + countrySelect.value + digits;
|
|
80
|
+
onNext(full, channel);
|
|
81
|
+
} else {
|
|
82
|
+
input.focus();
|
|
83
|
+
if (typeof input.reportValidity === "function") {
|
|
84
|
+
input.reportValidity();
|
|
85
|
+
}
|
|
73
86
|
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
form.addEventListener("submit", (e) => {
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
submitMobile("sms");
|
|
74
92
|
});
|
|
75
93
|
|
|
76
|
-
|
|
94
|
+
if (waBtn) {
|
|
95
|
+
waBtn.addEventListener("click", (e) => {
|
|
96
|
+
e.preventDefault();
|
|
97
|
+
submitMobile("whatsapp");
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
wrap.querySelector("button[data-action='back']").addEventListener("click", onBack);
|
|
77
102
|
|
|
78
103
|
return wrap;
|
|
79
104
|
}
|
|
@@ -1,79 +1,202 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Mobile Verification Screen — OTP entry
|
|
3
|
-
* v2.0 — warm, modern design
|
|
3
|
+
* v2.0 — warm, modern design with SMS and WhatsApp support
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
const SMARTPHONE_ICON =
|
|
7
|
-
const
|
|
8
|
-
const
|
|
6
|
+
const SMARTPHONE_ICON = "M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z";
|
|
7
|
+
const WHATSAPP_ICON = "M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51l-.57-.01c-.198 0-.52.074-.792.372s-1.04 1.016-1.04 2.479 1.065 2.876 1.213 3.074c.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.888 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L0 24l6.335-1.662c1.746.953 3.71 1.455 5.711 1.456h.005c6.554 0 11.89-5.336 11.893-11.893a11.821 11.821 0 00-3.48-8.413z";
|
|
8
|
+
const ARROW_LEFT = "M19 12H5m7 7l-7-7 7-7";
|
|
9
|
+
const ARROW_RIGHT = "M5 12h14m-7-7l7 7-7 7";
|
|
9
10
|
|
|
10
|
-
export function createMobileVerifyScreen(mobile, onNext, onBack) {
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
export function createMobileVerifyScreen(mobile, onNext, onBack, options = {}) {
|
|
12
|
+
let currentChannel = (options.channel || "sms").toLowerCase();
|
|
13
|
+
const onResend = typeof options.onResend === "function" ? options.onResend : null;
|
|
13
14
|
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
`).join('');
|
|
15
|
+
const wrap = document.createElement("div");
|
|
16
|
+
wrap.className = "wf-screen";
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
18
|
+
let cooldownTimer = null;
|
|
19
|
+
let cooldownSec = 0;
|
|
20
|
+
|
|
21
|
+
function getExpectedLength() {
|
|
22
|
+
return currentChannel === "whatsapp" ? 6 : 4;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getDescText() {
|
|
26
|
+
if (currentChannel === "whatsapp") {
|
|
27
|
+
return `Enter the 6-digit code sent via WhatsApp to <span class="wf-highlight">${mobile}</span>`;
|
|
28
|
+
}
|
|
29
|
+
return `Enter the code sent to <span class="wf-highlight">${mobile}</span>`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function renderScreen() {
|
|
33
|
+
const otpLength = getExpectedLength();
|
|
34
|
+
const isWhatsApp = currentChannel === "whatsapp";
|
|
35
|
+
|
|
36
|
+
wrap.innerHTML = `
|
|
37
|
+
<div class="wf-screen-header">
|
|
38
|
+
<div class="wf-icon-box ${isWhatsApp ? "wf-icon-box-whatsapp" : ""}">
|
|
39
|
+
${isWhatsApp
|
|
40
|
+
? `<svg viewBox="0 0 24 24" fill="currentColor"><path d="${WHATSAPP_ICON}"/></svg>`
|
|
41
|
+
: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${SMARTPHONE_ICON}"/></svg>`
|
|
42
|
+
}
|
|
43
|
+
</div>
|
|
44
|
+
<h2 class="wf-screen-title">Verify your mobile</h2>
|
|
45
|
+
<p class="wf-screen-desc" id="wf-verify-desc">${getDescText()}</p>
|
|
30
46
|
</div>
|
|
31
|
-
<
|
|
32
|
-
<
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
47
|
+
<form class="wf-form" id="wf-mobile-verify-form">
|
|
48
|
+
<div class="wf-field">
|
|
49
|
+
<label class="wf-label">Verification code</label>
|
|
50
|
+
<div class="wf-otp-row" id="wf-otp-container">
|
|
51
|
+
${Array(otpLength).fill(0).map((_, i) => `
|
|
52
|
+
<input type="text" inputmode="numeric" maxlength="1" class="wf-otp-input" id="wf-mobile-otp-${i}" data-idx="${i}" autocomplete="one-time-code" />
|
|
53
|
+
`).join("")}
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="wf-btn-row">
|
|
57
|
+
<button type="button" class="wf-btn wf-btn-outline wf-btn-lg" data-action="back" aria-label="Go back">
|
|
58
|
+
<svg class="wf-icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${ARROW_LEFT}"/></svg>
|
|
59
|
+
</button>
|
|
60
|
+
<button type="submit" class="wf-btn wf-btn-primary wf-btn-lg wf-btn-flex">
|
|
61
|
+
Verify
|
|
62
|
+
<svg class="wf-icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${ARROW_RIGHT}"/></svg>
|
|
63
|
+
</button>
|
|
64
|
+
</div>
|
|
65
|
+
</form>
|
|
66
|
+
<div class="wf-verify-footer">
|
|
67
|
+
<p class="wf-footer-link">
|
|
68
|
+
Didn't receive the code?
|
|
69
|
+
<button type="button" class="wf-link" id="wf-btn-resend-current">
|
|
70
|
+
${cooldownSec > 0 ? `Resend (${cooldownSec}s)` : (isWhatsApp ? "Resend WhatsApp OTP" : "Resend")}
|
|
71
|
+
</button>
|
|
72
|
+
</p>
|
|
73
|
+
<div class="wf-channel-switch-wrap">
|
|
74
|
+
${isWhatsApp
|
|
75
|
+
? `<button type="button" class="wf-link wf-channel-switch-btn" id="wf-btn-switch-channel">Send via SMS instead</button>`
|
|
76
|
+
: `<button type="button" class="wf-whatsapp-link wf-channel-switch-btn" id="wf-btn-switch-channel">
|
|
77
|
+
<svg class="wf-icon-sm" viewBox="0 0 24 24" fill="currentColor"><path d="${WHATSAPP_ICON}"/></svg>
|
|
78
|
+
Send WhatsApp OTP instead
|
|
79
|
+
</button>`
|
|
80
|
+
}
|
|
81
|
+
</div>
|
|
82
|
+
<div id="wf-resend-status" class="wf-status-message" style="display:none;"></div>
|
|
39
83
|
</div>
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const nextIdx = Math.min(digits.length, 3);
|
|
55
|
-
if (otpEls[nextIdx]) otpEls[nextIdx].focus();
|
|
84
|
+
`;
|
|
85
|
+
|
|
86
|
+
bindEvents();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function setStatus(msg, type = "success") {
|
|
90
|
+
const el = wrap.querySelector("#wf-resend-status");
|
|
91
|
+
if (!el) return;
|
|
92
|
+
el.textContent = msg;
|
|
93
|
+
el.className = `wf-status-message ${type}`;
|
|
94
|
+
el.style.display = "block";
|
|
95
|
+
setTimeout(() => {
|
|
96
|
+
if (el) el.style.display = "none";
|
|
97
|
+
}, 4500);
|
|
56
98
|
}
|
|
57
99
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
100
|
+
function startCooldown() {
|
|
101
|
+
cooldownSec = 30;
|
|
102
|
+
const resendBtn = wrap.querySelector("#wf-btn-resend-current");
|
|
103
|
+
if (resendBtn) {
|
|
104
|
+
resendBtn.disabled = true;
|
|
105
|
+
resendBtn.textContent = `Resend (${cooldownSec}s)`;
|
|
106
|
+
}
|
|
107
|
+
if (cooldownTimer) clearInterval(cooldownTimer);
|
|
108
|
+
cooldownTimer = setInterval(() => {
|
|
109
|
+
cooldownSec -= 1;
|
|
110
|
+
const btn = wrap.querySelector("#wf-btn-resend-current");
|
|
111
|
+
if (cooldownSec <= 0) {
|
|
112
|
+
clearInterval(cooldownTimer);
|
|
113
|
+
cooldownTimer = null;
|
|
114
|
+
if (btn) {
|
|
115
|
+
btn.disabled = false;
|
|
116
|
+
btn.textContent = currentChannel === "whatsapp" ? "Resend WhatsApp OTP" : "Resend";
|
|
117
|
+
}
|
|
118
|
+
} else if (btn) {
|
|
119
|
+
btn.textContent = `Resend (${cooldownSec}s)`;
|
|
120
|
+
}
|
|
121
|
+
}, 1000);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function triggerResend(targetChannel) {
|
|
125
|
+
if (!onResend) return;
|
|
126
|
+
try {
|
|
127
|
+
const isSwitch = targetChannel !== currentChannel;
|
|
128
|
+
currentChannel = targetChannel;
|
|
129
|
+
if (isSwitch) {
|
|
130
|
+
renderScreen();
|
|
131
|
+
}
|
|
132
|
+
setStatus("Sending verification code...", "info");
|
|
133
|
+
const res = await onResend(targetChannel);
|
|
134
|
+
const chanLabel = targetChannel === "whatsapp" ? "WhatsApp" : "SMS";
|
|
135
|
+
setStatus(res?.message || `Code sent via ${chanLabel}!`, "success");
|
|
136
|
+
startCooldown();
|
|
137
|
+
const firstOtp = wrap.querySelector("#wf-mobile-otp-0");
|
|
138
|
+
if (firstOtp) firstOtp.focus();
|
|
139
|
+
} catch (err) {
|
|
140
|
+
const msg = err?.data?.error || err?.message || "Failed to resend code";
|
|
141
|
+
setStatus(msg, "error");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function bindEvents() {
|
|
146
|
+
const form = wrap.querySelector("#wf-mobile-verify-form");
|
|
147
|
+
const otpEls = wrap.querySelectorAll(".wf-otp-input");
|
|
148
|
+
const otpLength = getExpectedLength();
|
|
149
|
+
|
|
150
|
+
function handleOtpPaste(e) {
|
|
151
|
+
e.preventDefault();
|
|
152
|
+
const pasted = (e.clipboardData || window.clipboardData).getData("text");
|
|
153
|
+
const digits = pasted.replace(/\D/g, "").slice(0, otpLength).split("");
|
|
154
|
+
digits.forEach((d, i) => { if (otpEls[i]) otpEls[i].value = d; });
|
|
155
|
+
const nextIdx = Math.min(digits.length, otpLength - 1);
|
|
156
|
+
if (otpEls[nextIdx]) otpEls[nextIdx].focus();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
otpEls.forEach((el, i) => {
|
|
160
|
+
el.addEventListener("paste", handleOtpPaste);
|
|
161
|
+
el.addEventListener("input", (e) => {
|
|
162
|
+
const v = e.target.value.replace(/\D/g, "").slice(0, 1);
|
|
163
|
+
e.target.value = v;
|
|
164
|
+
if (v && i < otpLength - 1) otpEls[i + 1].focus();
|
|
165
|
+
});
|
|
166
|
+
el.addEventListener("keydown", (e) => {
|
|
167
|
+
if (e.key === "Backspace" && !e.target.value && i > 0) otpEls[i - 1].focus();
|
|
168
|
+
});
|
|
64
169
|
});
|
|
65
|
-
|
|
66
|
-
|
|
170
|
+
|
|
171
|
+
form.addEventListener("submit", (e) => {
|
|
172
|
+
e.preventDefault();
|
|
173
|
+
const code = [...otpEls].map((o) => o.value).join("");
|
|
174
|
+
if (code.length === otpLength || (code.length >= 4 && code.length <= 6)) {
|
|
175
|
+
onNext(code, currentChannel);
|
|
176
|
+
}
|
|
67
177
|
});
|
|
68
|
-
});
|
|
69
178
|
|
|
70
|
-
|
|
71
|
-
e.preventDefault();
|
|
72
|
-
const code = [...otpEls].map((o) => o.value).join('');
|
|
73
|
-
if (code.length === 4) onNext(code);
|
|
74
|
-
});
|
|
179
|
+
wrap.querySelector("button[data-action='back']").addEventListener("click", onBack);
|
|
75
180
|
|
|
76
|
-
|
|
181
|
+
const resendBtn = wrap.querySelector("#wf-btn-resend-current");
|
|
182
|
+
if (resendBtn) {
|
|
183
|
+
if (cooldownSec > 0) resendBtn.disabled = true;
|
|
184
|
+
resendBtn.addEventListener("click", (e) => {
|
|
185
|
+
e.preventDefault();
|
|
186
|
+
if (cooldownSec <= 0) triggerResend(currentChannel);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const switchBtn = wrap.querySelector("#wf-btn-switch-channel");
|
|
191
|
+
if (switchBtn) {
|
|
192
|
+
switchBtn.addEventListener("click", (e) => {
|
|
193
|
+
e.preventDefault();
|
|
194
|
+
const nextChannel = currentChannel === "whatsapp" ? "sms" : "whatsapp";
|
|
195
|
+
triggerResend(nextChannel);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
77
199
|
|
|
200
|
+
renderScreen();
|
|
78
201
|
return wrap;
|
|
79
202
|
}
|