@progalaxyelabs/ngx-stonescriptphp-client 1.22.0 → 1.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, makeEnvironmentProviders, Injectable, Inject, Optional, EventEmitter, Output, Input, Component, input, output, signal, computed } from '@angular/core';
|
|
2
|
+
import { InjectionToken, makeEnvironmentProviders, Injectable, Inject, Optional, EventEmitter, Output, Input, ViewChildren, Component, input, output, signal, computed } from '@angular/core';
|
|
3
3
|
import { BehaviorSubject } from 'rxjs';
|
|
4
4
|
import * as i3 from '@angular/common';
|
|
5
5
|
import { CommonModule } from '@angular/common';
|
|
@@ -777,6 +777,108 @@ class ProgalaxyElabsAuth {
|
|
|
777
777
|
return { exists: false };
|
|
778
778
|
}
|
|
779
779
|
}
|
|
780
|
+
// -- OTP authentication --------------------------------------------------
|
|
781
|
+
async sendOtp(identifier) {
|
|
782
|
+
try {
|
|
783
|
+
const response = await fetch(`${this.host}/api/auth/otp/send`, {
|
|
784
|
+
method: 'POST',
|
|
785
|
+
headers: { 'Content-Type': 'application/json' },
|
|
786
|
+
body: JSON.stringify({ identifier })
|
|
787
|
+
});
|
|
788
|
+
const data = await response.json();
|
|
789
|
+
if (!response.ok) {
|
|
790
|
+
return {
|
|
791
|
+
success: false,
|
|
792
|
+
identifier_type: 'email',
|
|
793
|
+
masked_identifier: '',
|
|
794
|
+
expires_in: 0,
|
|
795
|
+
resend_after: 0,
|
|
796
|
+
...data
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
return {
|
|
800
|
+
success: data.success ?? true,
|
|
801
|
+
identifier_type: data.identifier_type,
|
|
802
|
+
masked_identifier: data.masked_identifier,
|
|
803
|
+
expires_in: data.expires_in,
|
|
804
|
+
resend_after: data.resend_after
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
catch {
|
|
808
|
+
return {
|
|
809
|
+
success: false,
|
|
810
|
+
identifier_type: 'email',
|
|
811
|
+
masked_identifier: '',
|
|
812
|
+
expires_in: 0,
|
|
813
|
+
resend_after: 0
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
async verifyOtp(identifier, code) {
|
|
818
|
+
try {
|
|
819
|
+
const response = await fetch(`${this.host}/api/auth/otp/verify`, {
|
|
820
|
+
method: 'POST',
|
|
821
|
+
headers: { 'Content-Type': 'application/json' },
|
|
822
|
+
body: JSON.stringify({ identifier, code })
|
|
823
|
+
});
|
|
824
|
+
const data = await response.json();
|
|
825
|
+
if (!response.ok) {
|
|
826
|
+
return { success: false, verified_token: '', ...data };
|
|
827
|
+
}
|
|
828
|
+
return {
|
|
829
|
+
success: data.success ?? true,
|
|
830
|
+
verified_token: data.verified_token
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
catch {
|
|
834
|
+
return { success: false, verified_token: '' };
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
async identityLogin(verifiedToken) {
|
|
838
|
+
try {
|
|
839
|
+
const response = await fetch(`${this.host}/api/identity/login`, {
|
|
840
|
+
method: 'POST',
|
|
841
|
+
headers: { 'Content-Type': 'application/json' },
|
|
842
|
+
body: JSON.stringify({
|
|
843
|
+
verified_token: verifiedToken,
|
|
844
|
+
platform: this.config.platformCode
|
|
845
|
+
})
|
|
846
|
+
});
|
|
847
|
+
// 404 means no identity found — caller should show registration form
|
|
848
|
+
if (response.status === 404) {
|
|
849
|
+
return { success: false, message: 'identity_not_found' };
|
|
850
|
+
}
|
|
851
|
+
const data = await response.json();
|
|
852
|
+
if (!response.ok) {
|
|
853
|
+
return { success: false, message: data.error || data.message || 'Login failed' };
|
|
854
|
+
}
|
|
855
|
+
return this.handleLoginResponse(data);
|
|
856
|
+
}
|
|
857
|
+
catch {
|
|
858
|
+
return { success: false, message: 'Network error. Please try again.' };
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
async identityRegister(verifiedToken, displayName) {
|
|
862
|
+
try {
|
|
863
|
+
const response = await fetch(`${this.host}/api/identity/register`, {
|
|
864
|
+
method: 'POST',
|
|
865
|
+
headers: { 'Content-Type': 'application/json' },
|
|
866
|
+
body: JSON.stringify({
|
|
867
|
+
verified_token: verifiedToken,
|
|
868
|
+
display_name: displayName,
|
|
869
|
+
platform: this.config.platformCode
|
|
870
|
+
})
|
|
871
|
+
});
|
|
872
|
+
const data = await response.json();
|
|
873
|
+
if (!response.ok) {
|
|
874
|
+
return { success: false, message: data.error || data.message || 'Registration failed' };
|
|
875
|
+
}
|
|
876
|
+
return this.handleLoginResponse(data);
|
|
877
|
+
}
|
|
878
|
+
catch {
|
|
879
|
+
return { success: false, message: 'Network error. Please try again.' };
|
|
880
|
+
}
|
|
881
|
+
}
|
|
780
882
|
// -- OAuth ----------------------------------------------------------------
|
|
781
883
|
async loginWithProvider(provider) {
|
|
782
884
|
return new Promise((resolve) => {
|
|
@@ -877,7 +979,8 @@ class ProgalaxyElabsAuth {
|
|
|
877
979
|
if (!raw)
|
|
878
980
|
return undefined;
|
|
879
981
|
return {
|
|
880
|
-
email: raw.email,
|
|
982
|
+
email: raw.email ?? '',
|
|
983
|
+
phone: raw.phone,
|
|
881
984
|
display_name: raw.display_name ?? raw.email?.split('@')[0] ?? '',
|
|
882
985
|
photo_url: raw.photo_url ?? raw.picture,
|
|
883
986
|
is_email_verified: raw.is_email_verified ?? false
|
|
@@ -1201,6 +1304,37 @@ class AuthService {
|
|
|
1201
1304
|
getCurrentUser() {
|
|
1202
1305
|
return this.userSubject.value;
|
|
1203
1306
|
}
|
|
1307
|
+
// ── OTP authentication ─────────────────────────────────────────────────────
|
|
1308
|
+
async sendOtp(identifier) {
|
|
1309
|
+
if (!this.plugin.sendOtp) {
|
|
1310
|
+
return { success: false, identifier_type: 'email', masked_identifier: '', expires_in: 0, resend_after: 0 };
|
|
1311
|
+
}
|
|
1312
|
+
return this.plugin.sendOtp(identifier);
|
|
1313
|
+
}
|
|
1314
|
+
async verifyOtp(identifier, code) {
|
|
1315
|
+
if (!this.plugin.verifyOtp) {
|
|
1316
|
+
return { success: false, verified_token: '' };
|
|
1317
|
+
}
|
|
1318
|
+
return this.plugin.verifyOtp(identifier, code);
|
|
1319
|
+
}
|
|
1320
|
+
async identityLogin(verifiedToken) {
|
|
1321
|
+
if (!this.plugin.identityLogin) {
|
|
1322
|
+
return { success: false, message: 'OTP login not supported by the configured auth plugin' };
|
|
1323
|
+
}
|
|
1324
|
+
const result = await this.plugin.identityLogin(verifiedToken);
|
|
1325
|
+
if (result.success)
|
|
1326
|
+
this.storeAuthResult(result);
|
|
1327
|
+
return result;
|
|
1328
|
+
}
|
|
1329
|
+
async identityRegister(verifiedToken, displayName) {
|
|
1330
|
+
if (!this.plugin.identityRegister) {
|
|
1331
|
+
return { success: false, message: 'OTP registration not supported by the configured auth plugin' };
|
|
1332
|
+
}
|
|
1333
|
+
const result = await this.plugin.identityRegister(verifiedToken, displayName);
|
|
1334
|
+
if (result.success)
|
|
1335
|
+
this.storeAuthResult(result);
|
|
1336
|
+
return result;
|
|
1337
|
+
}
|
|
1204
1338
|
// ── Multi-tenant operations ───────────────────────────────────────────────
|
|
1205
1339
|
async getTenantMemberships(serverName) {
|
|
1206
1340
|
if (!this.plugin.getTenantMemberships)
|
|
@@ -2003,6 +2137,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
2003
2137
|
class TenantLoginComponent {
|
|
2004
2138
|
auth;
|
|
2005
2139
|
providerRegistry;
|
|
2140
|
+
otpInputs;
|
|
2006
2141
|
// Component Configuration
|
|
2007
2142
|
title = 'Sign In';
|
|
2008
2143
|
providers = ['google'];
|
|
@@ -2032,6 +2167,22 @@ class TenantLoginComponent {
|
|
|
2032
2167
|
showPassword = false;
|
|
2033
2168
|
useOAuth = true;
|
|
2034
2169
|
oauthProviders = [];
|
|
2170
|
+
// Effective OAuth providers (filtered for Android WebView)
|
|
2171
|
+
effectiveOauthProviders = [];
|
|
2172
|
+
// OTP State
|
|
2173
|
+
otpActive = false;
|
|
2174
|
+
otpStep = 'identifier';
|
|
2175
|
+
otpIdentifier = '';
|
|
2176
|
+
otpIdentifierHint = '';
|
|
2177
|
+
otpNormalizedIdentifier = ''; // E.164 for phone, as-is for email
|
|
2178
|
+
otpMaskedIdentifier = '';
|
|
2179
|
+
otpDigits = ['', '', '', '', '', ''];
|
|
2180
|
+
otpVerifiedToken = '';
|
|
2181
|
+
otpDisplayName = '';
|
|
2182
|
+
otpResendCountdown = 0;
|
|
2183
|
+
otpResendTimer = null;
|
|
2184
|
+
// Android WebView detection
|
|
2185
|
+
isAndroidWebView = false;
|
|
2035
2186
|
// Tenant Selection State
|
|
2036
2187
|
showingTenantSelector = false;
|
|
2037
2188
|
memberships = [];
|
|
@@ -2046,17 +2197,32 @@ class TenantLoginComponent {
|
|
|
2046
2197
|
this.error = 'Configuration Error: No authentication providers specified.';
|
|
2047
2198
|
throw new Error('TenantLoginComponent requires providers input.');
|
|
2048
2199
|
}
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2200
|
+
// Detect Android WebView
|
|
2201
|
+
this.isAndroidWebView = /wv|Android.*Version\//.test(navigator.userAgent);
|
|
2202
|
+
// Filter out 'otp' and 'emailPassword' for OAuth list
|
|
2203
|
+
this.oauthProviders = this.providers.filter(p => p !== 'emailPassword' && p !== 'otp');
|
|
2204
|
+
// Auto-hide Google on Android WebView
|
|
2205
|
+
this.effectiveOauthProviders = this.isAndroidWebView
|
|
2206
|
+
? this.oauthProviders.filter(p => p !== 'google')
|
|
2207
|
+
: [...this.oauthProviders];
|
|
2208
|
+
// If OTP is configured, it becomes the primary login method
|
|
2209
|
+
if (this.isProviderEnabled('otp')) {
|
|
2210
|
+
this.otpActive = true;
|
|
2211
|
+
}
|
|
2212
|
+
// If only emailPassword is available (no OTP, no OAuth), use it by default
|
|
2213
|
+
if (!this.otpActive && this.effectiveOauthProviders.length === 0 && this.isProviderEnabled('emailPassword')) {
|
|
2052
2214
|
this.useOAuth = false;
|
|
2053
2215
|
}
|
|
2054
2216
|
// Prefill email if provided (for account linking flow)
|
|
2055
2217
|
if (this.prefillEmail) {
|
|
2056
2218
|
this.email = this.prefillEmail;
|
|
2057
|
-
this.useOAuth = false;
|
|
2219
|
+
this.useOAuth = false;
|
|
2220
|
+
this.otpActive = false; // Switch to email/password form for linking
|
|
2058
2221
|
}
|
|
2059
2222
|
}
|
|
2223
|
+
ngOnDestroy() {
|
|
2224
|
+
this.clearResendTimer();
|
|
2225
|
+
}
|
|
2060
2226
|
isProviderEnabled(provider) {
|
|
2061
2227
|
return this.providers.includes(provider);
|
|
2062
2228
|
}
|
|
@@ -2237,6 +2403,244 @@ class TenantLoginComponent {
|
|
|
2237
2403
|
this.loading = false;
|
|
2238
2404
|
}
|
|
2239
2405
|
}
|
|
2406
|
+
// ── OTP methods ────────────────────────────────────────────────────────────
|
|
2407
|
+
/** Detect identifier type from input and show hint */
|
|
2408
|
+
onOtpIdentifierChange() {
|
|
2409
|
+
const value = this.otpIdentifier.trim();
|
|
2410
|
+
if (!value) {
|
|
2411
|
+
this.otpIdentifierHint = '';
|
|
2412
|
+
return;
|
|
2413
|
+
}
|
|
2414
|
+
const detected = this.detectIdentifierType(value);
|
|
2415
|
+
if (detected === 'email') {
|
|
2416
|
+
this.otpIdentifierHint = 'OTP will be sent to this email';
|
|
2417
|
+
}
|
|
2418
|
+
else if (detected === 'phone') {
|
|
2419
|
+
const digits = value.replace(/\D/g, '');
|
|
2420
|
+
if (digits.length === 10) {
|
|
2421
|
+
this.otpIdentifierHint = 'OTP will be sent to +91 ' + digits;
|
|
2422
|
+
}
|
|
2423
|
+
else {
|
|
2424
|
+
this.otpIdentifierHint = 'OTP will be sent to this number';
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
else {
|
|
2428
|
+
this.otpIdentifierHint = '';
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
/** Send OTP to the entered identifier */
|
|
2432
|
+
async onOtpSend() {
|
|
2433
|
+
const raw = this.otpIdentifier.trim();
|
|
2434
|
+
if (!raw) {
|
|
2435
|
+
this.error = 'Please enter your email or phone number';
|
|
2436
|
+
return;
|
|
2437
|
+
}
|
|
2438
|
+
const type = this.detectIdentifierType(raw);
|
|
2439
|
+
if (!type) {
|
|
2440
|
+
this.error = 'Please enter a valid email address or phone number';
|
|
2441
|
+
return;
|
|
2442
|
+
}
|
|
2443
|
+
// Normalize: auto-prepend +91 for 10-digit Indian numbers
|
|
2444
|
+
if (type === 'phone') {
|
|
2445
|
+
const digits = raw.replace(/\D/g, '');
|
|
2446
|
+
this.otpNormalizedIdentifier = digits.length === 10 ? `+91${digits}` : (raw.startsWith('+') ? raw : `+${digits}`);
|
|
2447
|
+
}
|
|
2448
|
+
else {
|
|
2449
|
+
this.otpNormalizedIdentifier = raw;
|
|
2450
|
+
}
|
|
2451
|
+
this.loading = true;
|
|
2452
|
+
this.error = '';
|
|
2453
|
+
try {
|
|
2454
|
+
const result = await this.auth.sendOtp(this.otpNormalizedIdentifier);
|
|
2455
|
+
if (!result.success) {
|
|
2456
|
+
this.error = 'Failed to send OTP. Please try again.';
|
|
2457
|
+
return;
|
|
2458
|
+
}
|
|
2459
|
+
this.otpMaskedIdentifier = result.masked_identifier;
|
|
2460
|
+
this.otpDigits = ['', '', '', '', '', ''];
|
|
2461
|
+
this.otpStep = 'code';
|
|
2462
|
+
this.startResendCountdown(result.resend_after || 60);
|
|
2463
|
+
// Focus first digit input after view update
|
|
2464
|
+
setTimeout(() => this.focusOtpInput(0), 50);
|
|
2465
|
+
}
|
|
2466
|
+
catch (err) {
|
|
2467
|
+
this.error = err.message || 'Failed to send OTP';
|
|
2468
|
+
}
|
|
2469
|
+
finally {
|
|
2470
|
+
this.loading = false;
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
/** Verify the entered OTP code */
|
|
2474
|
+
async onOtpVerify() {
|
|
2475
|
+
const code = this.otpCode;
|
|
2476
|
+
if (code.length < 6) {
|
|
2477
|
+
this.error = 'Please enter the complete 6-digit code';
|
|
2478
|
+
return;
|
|
2479
|
+
}
|
|
2480
|
+
this.loading = true;
|
|
2481
|
+
this.error = '';
|
|
2482
|
+
try {
|
|
2483
|
+
const verifyResult = await this.auth.verifyOtp(this.otpNormalizedIdentifier, code);
|
|
2484
|
+
if (!verifyResult.success) {
|
|
2485
|
+
this.error = 'Invalid code. Please try again.';
|
|
2486
|
+
return;
|
|
2487
|
+
}
|
|
2488
|
+
this.otpVerifiedToken = verifyResult.verified_token;
|
|
2489
|
+
// Auto-attempt login
|
|
2490
|
+
const loginResult = await this.auth.identityLogin(this.otpVerifiedToken);
|
|
2491
|
+
if (loginResult.success) {
|
|
2492
|
+
// Existing user — proceed with standard post-auth flow
|
|
2493
|
+
await this.handlePostAuthFlow(loginResult);
|
|
2494
|
+
return;
|
|
2495
|
+
}
|
|
2496
|
+
if (loginResult.message === 'identity_not_found') {
|
|
2497
|
+
// New user — show registration form
|
|
2498
|
+
this.otpStep = 'register';
|
|
2499
|
+
return;
|
|
2500
|
+
}
|
|
2501
|
+
// Other login error
|
|
2502
|
+
this.error = loginResult.message || 'Login failed. Please try again.';
|
|
2503
|
+
}
|
|
2504
|
+
catch (err) {
|
|
2505
|
+
this.error = err.message || 'Verification failed';
|
|
2506
|
+
}
|
|
2507
|
+
finally {
|
|
2508
|
+
this.loading = false;
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
/** Register a new identity after OTP verification */
|
|
2512
|
+
async onOtpRegister() {
|
|
2513
|
+
const name = this.otpDisplayName.trim();
|
|
2514
|
+
if (!name) {
|
|
2515
|
+
this.error = 'Please enter your name';
|
|
2516
|
+
return;
|
|
2517
|
+
}
|
|
2518
|
+
this.loading = true;
|
|
2519
|
+
this.error = '';
|
|
2520
|
+
try {
|
|
2521
|
+
const result = await this.auth.identityRegister(this.otpVerifiedToken, name);
|
|
2522
|
+
if (!result.success) {
|
|
2523
|
+
this.error = result.message || 'Registration failed';
|
|
2524
|
+
return;
|
|
2525
|
+
}
|
|
2526
|
+
// New identity created — emit onboarding event
|
|
2527
|
+
const identifierType = this.detectIdentifierType(this.otpIdentifier.trim());
|
|
2528
|
+
this.needsOnboarding.emit({
|
|
2529
|
+
auth_method: 'otp',
|
|
2530
|
+
is_new_identity: true,
|
|
2531
|
+
identity: {
|
|
2532
|
+
email: identifierType === 'email' ? this.otpNormalizedIdentifier : '',
|
|
2533
|
+
phone: identifierType === 'phone' ? this.otpNormalizedIdentifier : undefined,
|
|
2534
|
+
display_name: name,
|
|
2535
|
+
},
|
|
2536
|
+
});
|
|
2537
|
+
}
|
|
2538
|
+
catch (err) {
|
|
2539
|
+
this.error = err.message || 'Registration failed';
|
|
2540
|
+
}
|
|
2541
|
+
finally {
|
|
2542
|
+
this.loading = false;
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
/** Resend the OTP */
|
|
2546
|
+
onOtpResend(event) {
|
|
2547
|
+
event.preventDefault();
|
|
2548
|
+
this.onOtpSend();
|
|
2549
|
+
}
|
|
2550
|
+
/** Go back to identifier entry */
|
|
2551
|
+
onOtpBack(event) {
|
|
2552
|
+
event.preventDefault();
|
|
2553
|
+
this.otpStep = 'identifier';
|
|
2554
|
+
this.otpDigits = ['', '', '', '', '', ''];
|
|
2555
|
+
this.otpVerifiedToken = '';
|
|
2556
|
+
this.error = '';
|
|
2557
|
+
this.clearResendTimer();
|
|
2558
|
+
}
|
|
2559
|
+
// ── OTP digit input handling ─────────────────────────────────────────────
|
|
2560
|
+
onOtpDigitInput(event, index) {
|
|
2561
|
+
const input = event.target;
|
|
2562
|
+
const value = input.value.replace(/\D/g, '');
|
|
2563
|
+
this.otpDigits[index] = value ? value[0] : '';
|
|
2564
|
+
input.value = this.otpDigits[index];
|
|
2565
|
+
// Auto-advance to next input
|
|
2566
|
+
if (value && index < 5) {
|
|
2567
|
+
this.focusOtpInput(index + 1);
|
|
2568
|
+
}
|
|
2569
|
+
// Auto-verify when all 6 digits entered
|
|
2570
|
+
if (this.otpCode.length === 6) {
|
|
2571
|
+
this.onOtpVerify();
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
onOtpDigitKeydown(event, index) {
|
|
2575
|
+
if (event.key === 'Backspace' && !this.otpDigits[index] && index > 0) {
|
|
2576
|
+
this.focusOtpInput(index - 1);
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
onOtpPaste(event) {
|
|
2580
|
+
event.preventDefault();
|
|
2581
|
+
const pasted = (event.clipboardData?.getData('text') || '').replace(/\D/g, '').slice(0, 6);
|
|
2582
|
+
for (let i = 0; i < 6; i++) {
|
|
2583
|
+
this.otpDigits[i] = pasted[i] || '';
|
|
2584
|
+
}
|
|
2585
|
+
// Update all input elements
|
|
2586
|
+
const inputs = this.otpInputs?.toArray();
|
|
2587
|
+
if (inputs) {
|
|
2588
|
+
for (let i = 0; i < 6; i++) {
|
|
2589
|
+
inputs[i].nativeElement.value = this.otpDigits[i];
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
if (pasted.length >= 6) {
|
|
2593
|
+
this.onOtpVerify();
|
|
2594
|
+
}
|
|
2595
|
+
else {
|
|
2596
|
+
this.focusOtpInput(Math.min(pasted.length, 5));
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
get otpCode() {
|
|
2600
|
+
return this.otpDigits.join('');
|
|
2601
|
+
}
|
|
2602
|
+
// ── OTP helpers ──────────────────────────────────────────────────────────
|
|
2603
|
+
detectIdentifierType(value) {
|
|
2604
|
+
if (value.includes('@')) {
|
|
2605
|
+
// Basic email validation
|
|
2606
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? 'email' : null;
|
|
2607
|
+
}
|
|
2608
|
+
const digits = value.replace(/\D/g, '');
|
|
2609
|
+
if (digits.length >= 10 && digits.length <= 15) {
|
|
2610
|
+
return 'phone';
|
|
2611
|
+
}
|
|
2612
|
+
return null;
|
|
2613
|
+
}
|
|
2614
|
+
focusOtpInput(index) {
|
|
2615
|
+
const inputs = this.otpInputs?.toArray();
|
|
2616
|
+
if (inputs && inputs[index]) {
|
|
2617
|
+
inputs[index].nativeElement.focus();
|
|
2618
|
+
inputs[index].nativeElement.select();
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
startResendCountdown(seconds) {
|
|
2622
|
+
this.clearResendTimer();
|
|
2623
|
+
this.otpResendCountdown = seconds;
|
|
2624
|
+
this.otpResendTimer = setInterval(() => {
|
|
2625
|
+
this.otpResendCountdown--;
|
|
2626
|
+
if (this.otpResendCountdown <= 0) {
|
|
2627
|
+
this.clearResendTimer();
|
|
2628
|
+
}
|
|
2629
|
+
}, 1000);
|
|
2630
|
+
}
|
|
2631
|
+
clearResendTimer() {
|
|
2632
|
+
if (this.otpResendTimer) {
|
|
2633
|
+
clearInterval(this.otpResendTimer);
|
|
2634
|
+
this.otpResendTimer = null;
|
|
2635
|
+
}
|
|
2636
|
+
this.otpResendCountdown = 0;
|
|
2637
|
+
}
|
|
2638
|
+
formatCountdown(seconds) {
|
|
2639
|
+
const m = Math.floor(seconds / 60);
|
|
2640
|
+
const s = seconds % 60;
|
|
2641
|
+
return `${m}:${s.toString().padStart(2, '0')}`;
|
|
2642
|
+
}
|
|
2643
|
+
// ── Formatting helpers ───────────────────────────────────────────────────
|
|
2240
2644
|
formatRole(role) {
|
|
2241
2645
|
return role.charAt(0).toUpperCase() + role.slice(1);
|
|
2242
2646
|
}
|
|
@@ -2265,14 +2669,125 @@ class TenantLoginComponent {
|
|
|
2265
2669
|
this.createTenant.emit();
|
|
2266
2670
|
}
|
|
2267
2671
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: TenantLoginComponent, deps: [{ token: AuthService }, { token: ProviderRegistryService }], target: i0.ɵɵFactoryTarget.Component });
|
|
2268
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: TenantLoginComponent, isStandalone: true, selector: "lib-tenant-login", inputs: { title: "title", providers: "providers", showTenantSelector: "showTenantSelector", autoSelectSingleTenant: "autoSelectSingleTenant", prefillEmail: "prefillEmail", allowTenantCreation: "allowTenantCreation", tenantSelectorTitle: "tenantSelectorTitle", tenantSelectorDescription: "tenantSelectorDescription", continueButtonText: "continueButtonText", registerLinkText: "registerLinkText", registerLinkAction: "registerLinkAction", createTenantLinkText: "createTenantLinkText", createTenantLinkAction: "createTenantLinkAction" }, outputs: { tenantSelected: "tenantSelected", needsOnboarding: "needsOnboarding", createTenant: "createTenant" }, ngImport: i0, template: `
|
|
2672
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: TenantLoginComponent, isStandalone: true, selector: "lib-tenant-login", inputs: { title: "title", providers: "providers", showTenantSelector: "showTenantSelector", autoSelectSingleTenant: "autoSelectSingleTenant", prefillEmail: "prefillEmail", allowTenantCreation: "allowTenantCreation", tenantSelectorTitle: "tenantSelectorTitle", tenantSelectorDescription: "tenantSelectorDescription", continueButtonText: "continueButtonText", registerLinkText: "registerLinkText", registerLinkAction: "registerLinkAction", createTenantLinkText: "createTenantLinkText", createTenantLinkAction: "createTenantLinkAction" }, outputs: { tenantSelected: "tenantSelected", needsOnboarding: "needsOnboarding", createTenant: "createTenant" }, viewQueries: [{ propertyName: "otpInputs", predicate: ["otpInput"], descendants: true }], ngImport: i0, template: `
|
|
2269
2673
|
<div class="tenant-login-dialog">
|
|
2270
2674
|
@if (!showingTenantSelector) {
|
|
2271
2675
|
<!-- Step 1: Authentication -->
|
|
2272
2676
|
<h2 class="login-title">{{ title }}</h2>
|
|
2273
2677
|
|
|
2678
|
+
<!-- OTP Flow -->
|
|
2679
|
+
@if (isProviderEnabled('otp') && otpActive) {
|
|
2680
|
+
<!-- OTP Step 1: Identifier entry -->
|
|
2681
|
+
@if (otpStep === 'identifier') {
|
|
2682
|
+
<form (ngSubmit)="onOtpSend()" class="otp-form">
|
|
2683
|
+
<div class="form-group">
|
|
2684
|
+
<input
|
|
2685
|
+
[(ngModel)]="otpIdentifier"
|
|
2686
|
+
name="otpIdentifier"
|
|
2687
|
+
placeholder="Enter Email or Phone Number"
|
|
2688
|
+
type="text"
|
|
2689
|
+
required
|
|
2690
|
+
autocomplete="email tel"
|
|
2691
|
+
class="form-control"
|
|
2692
|
+
(input)="onOtpIdentifierChange()">
|
|
2693
|
+
@if (otpIdentifierHint) {
|
|
2694
|
+
<div class="field-hint">{{ otpIdentifierHint }}</div>
|
|
2695
|
+
}
|
|
2696
|
+
</div>
|
|
2697
|
+
<button
|
|
2698
|
+
type="submit"
|
|
2699
|
+
[disabled]="loading || !otpIdentifier.trim()"
|
|
2700
|
+
class="btn btn-primary btn-block">
|
|
2701
|
+
{{ loading ? 'Sending...' : 'Send OTP' }}
|
|
2702
|
+
</button>
|
|
2703
|
+
</form>
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
<!-- OTP Step 2: Code entry -->
|
|
2707
|
+
@if (otpStep === 'code') {
|
|
2708
|
+
<div class="otp-code-section">
|
|
2709
|
+
<p class="otp-subtitle">
|
|
2710
|
+
Enter the 6-digit code sent to
|
|
2711
|
+
<strong>{{ otpMaskedIdentifier }}</strong>
|
|
2712
|
+
</p>
|
|
2713
|
+
<div class="otp-digits">
|
|
2714
|
+
@for (digit of otpDigits; track $index; let i = $index) {
|
|
2715
|
+
<input
|
|
2716
|
+
#otpInput
|
|
2717
|
+
type="text"
|
|
2718
|
+
inputmode="numeric"
|
|
2719
|
+
maxlength="1"
|
|
2720
|
+
class="otp-digit-input"
|
|
2721
|
+
[value]="otpDigits[i]"
|
|
2722
|
+
(input)="onOtpDigitInput($event, i)"
|
|
2723
|
+
(keydown)="onOtpDigitKeydown($event, i)"
|
|
2724
|
+
(paste)="onOtpPaste($event)">
|
|
2725
|
+
}
|
|
2726
|
+
</div>
|
|
2727
|
+
<div class="otp-actions">
|
|
2728
|
+
<button
|
|
2729
|
+
type="button"
|
|
2730
|
+
(click)="onOtpVerify()"
|
|
2731
|
+
[disabled]="loading || otpCode.length < 6"
|
|
2732
|
+
class="btn btn-primary btn-block">
|
|
2733
|
+
{{ loading ? 'Verifying...' : 'Verify' }}
|
|
2734
|
+
</button>
|
|
2735
|
+
</div>
|
|
2736
|
+
<div class="otp-resend">
|
|
2737
|
+
@if (otpResendCountdown > 0) {
|
|
2738
|
+
<span class="resend-timer">
|
|
2739
|
+
Resend in {{ formatCountdown(otpResendCountdown) }}
|
|
2740
|
+
</span>
|
|
2741
|
+
} @else {
|
|
2742
|
+
<a href="#" (click)="onOtpResend($event)" class="resend-link">
|
|
2743
|
+
Resend OTP
|
|
2744
|
+
</a>
|
|
2745
|
+
}
|
|
2746
|
+
</div>
|
|
2747
|
+
<div class="otp-back">
|
|
2748
|
+
<a href="#" (click)="onOtpBack($event)">
|
|
2749
|
+
Use a different email or phone
|
|
2750
|
+
</a>
|
|
2751
|
+
</div>
|
|
2752
|
+
</div>
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
<!-- OTP Step 3: Registration (new user) -->
|
|
2756
|
+
@if (otpStep === 'register') {
|
|
2757
|
+
<div class="otp-register-section">
|
|
2758
|
+
<p class="otp-subtitle">
|
|
2759
|
+
Welcome! Enter your name to get started.
|
|
2760
|
+
</p>
|
|
2761
|
+
<form (ngSubmit)="onOtpRegister()" class="otp-form">
|
|
2762
|
+
<div class="form-group">
|
|
2763
|
+
<input
|
|
2764
|
+
[(ngModel)]="otpDisplayName"
|
|
2765
|
+
name="displayName"
|
|
2766
|
+
placeholder="Your Name"
|
|
2767
|
+
type="text"
|
|
2768
|
+
required
|
|
2769
|
+
class="form-control">
|
|
2770
|
+
</div>
|
|
2771
|
+
<button
|
|
2772
|
+
type="submit"
|
|
2773
|
+
[disabled]="loading || !otpDisplayName.trim()"
|
|
2774
|
+
class="btn btn-primary btn-block">
|
|
2775
|
+
{{ loading ? 'Creating account...' : 'Continue' }}
|
|
2776
|
+
</button>
|
|
2777
|
+
</form>
|
|
2778
|
+
</div>
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
<!-- Divider before other providers -->
|
|
2782
|
+
@if (effectiveOauthProviders.length > 0 || isProviderEnabled('emailPassword')) {
|
|
2783
|
+
<div class="divider">
|
|
2784
|
+
<span>OR</span>
|
|
2785
|
+
</div>
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
|
|
2274
2789
|
<!-- Email/Password Form (if enabled) -->
|
|
2275
|
-
@if (isProviderEnabled('emailPassword') && !useOAuth) {
|
|
2790
|
+
@if (isProviderEnabled('emailPassword') && !useOAuth && !otpActive) {
|
|
2276
2791
|
<form (ngSubmit)="onEmailLogin()" class="email-form">
|
|
2277
2792
|
<div class="form-group">
|
|
2278
2793
|
<input
|
|
@@ -2296,7 +2811,7 @@ class TenantLoginComponent {
|
|
|
2296
2811
|
class="password-toggle"
|
|
2297
2812
|
(click)="showPassword = !showPassword"
|
|
2298
2813
|
[attr.aria-label]="showPassword ? 'Hide password' : 'Show password'">
|
|
2299
|
-
{{ showPassword ? '
|
|
2814
|
+
{{ showPassword ? '👁' : '👁‍🗨' }}
|
|
2300
2815
|
</button>
|
|
2301
2816
|
</div>
|
|
2302
2817
|
<button
|
|
@@ -2308,7 +2823,7 @@ class TenantLoginComponent {
|
|
|
2308
2823
|
</form>
|
|
2309
2824
|
|
|
2310
2825
|
<!-- Divider -->
|
|
2311
|
-
@if (
|
|
2826
|
+
@if (effectiveOauthProviders.length > 0) {
|
|
2312
2827
|
<div class="divider">
|
|
2313
2828
|
<span>OR</span>
|
|
2314
2829
|
</div>
|
|
@@ -2316,9 +2831,9 @@ class TenantLoginComponent {
|
|
|
2316
2831
|
}
|
|
2317
2832
|
|
|
2318
2833
|
<!-- OAuth Providers -->
|
|
2319
|
-
@if (
|
|
2834
|
+
@if (effectiveOauthProviders.length > 0 && !otpActive && (useOAuth || !isProviderEnabled('emailPassword'))) {
|
|
2320
2835
|
<div class="oauth-buttons">
|
|
2321
|
-
@for (provider of
|
|
2836
|
+
@for (provider of effectiveOauthProviders; track provider) {
|
|
2322
2837
|
<button
|
|
2323
2838
|
type="button"
|
|
2324
2839
|
(click)="onOAuthLogin(provider)"
|
|
@@ -2336,7 +2851,7 @@ class TenantLoginComponent {
|
|
|
2336
2851
|
</div>
|
|
2337
2852
|
|
|
2338
2853
|
<!-- Switch to Email/Password -->
|
|
2339
|
-
@if (isProviderEnabled('emailPassword') &&
|
|
2854
|
+
@if (isProviderEnabled('emailPassword') && effectiveOauthProviders.length > 0) {
|
|
2340
2855
|
<div class="switch-method">
|
|
2341
2856
|
<a href="#" (click)="toggleAuthMethod($event)">
|
|
2342
2857
|
{{ useOAuth ? 'Use email/password instead' : 'Use OAuth instead' }}
|
|
@@ -2431,7 +2946,7 @@ class TenantLoginComponent {
|
|
|
2431
2946
|
</div>
|
|
2432
2947
|
}
|
|
2433
2948
|
</div>
|
|
2434
|
-
`, isInline: true, styles: [".tenant-login-dialog{padding:24px;max-width:450px;position:relative}.login-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.welcome-message{margin-bottom:16px;padding:12px;background:#e8f5e9;border-radius:4px;text-align:center;font-size:14px;color:#2e7d32}.selector-description{margin-bottom:20px;font-size:14px;color:#666;text-align:center}.email-form,.form-group{margin-bottom:16px}.password-group{position:relative}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.password-input{padding-right:45px}.password-toggle{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;font-size:18px;padding:8px;line-height:1;opacity:.6;transition:opacity .2s}.password-toggle:hover{opacity:1}.password-toggle:focus{outline:2px solid #4285f4;outline-offset:2px;border-radius:4px}.form-control:focus{outline:none;border-color:#4285f4}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.divider{margin:16px 0;text-align:center;position:relative}.divider:before{content:\"\";position:absolute;top:50%;left:0;right:0;height:1px;background:#ddd}.divider span{background:#fff;padding:0 12px;position:relative;color:#666;font-size:12px}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.btn-zoho{background-color:#f0483e;color:#fff;border:1px solid #d63b32}.btn-zoho:hover{background-color:#d63b32}.oauth-icon{font-size:18px}.switch-method{margin-top:12px;text-align:center;font-size:14px}.switch-method a{color:#4285f4;text-decoration:none}.switch-method a:hover{text-decoration:underline}.tenant-list{margin-bottom:20px;display:flex;flex-direction:column;gap:12px}.tenant-item{display:flex;align-items:flex-start;gap:12px;padding:16px;border:2px solid #e0e0e0;border-radius:6px;cursor:pointer;transition:all .2s}.tenant-item:hover{border-color:#4285f4;background:#f8f9ff}.tenant-item.selected{border-color:#4285f4;background:#e8f0fe}.tenant-radio{flex-shrink:0;padding-top:2px}.tenant-radio input[type=radio]{width:18px;height:18px;cursor:pointer}.tenant-info{flex:1}.tenant-name{font-size:16px;font-weight:500;color:#333;margin-bottom:4px}.tenant-meta{font-size:13px;color:#666}.tenant-role{font-weight:500;color:#4285f4}.tenant-separator{margin:0 6px}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.register-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.register-link a{color:#4285f4;text-decoration:none}.register-link a:hover{text-decoration:underline}.create-tenant-link{margin-top:16px;padding-top:16px;border-top:1px solid #e0e0e0;text-align:center;font-size:14px;color:#666}.create-tenant-link a{color:#4285f4;text-decoration:none;font-weight:500}.create-tenant-link a:hover{text-decoration:underline}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i3.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i4.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i4.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i4.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i4.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i4.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i4.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i4.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }] });
|
|
2949
|
+
`, isInline: true, styles: [".tenant-login-dialog{padding:24px;max-width:450px;position:relative}.login-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.welcome-message{margin-bottom:16px;padding:12px;background:#e8f5e9;border-radius:4px;text-align:center;font-size:14px;color:#2e7d32}.selector-description{margin-bottom:20px;font-size:14px;color:#666;text-align:center}.email-form,.otp-form,.form-group{margin-bottom:16px}.password-group{position:relative}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.password-input{padding-right:45px}.password-toggle{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;font-size:18px;padding:8px;line-height:1;opacity:.6;transition:opacity .2s}.password-toggle:hover{opacity:1}.password-toggle:focus{outline:2px solid #4285f4;outline-offset:2px;border-radius:4px}.form-control:focus{outline:none;border-color:#4285f4}.field-hint{margin-top:4px;font-size:12px;color:#888}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.divider{margin:16px 0;text-align:center;position:relative}.divider:before{content:\"\";position:absolute;top:50%;left:0;right:0;height:1px;background:#ddd}.divider span{background:#fff;padding:0 12px;position:relative;color:#666;font-size:12px}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.btn-zoho{background-color:#f0483e;color:#fff;border:1px solid #d63b32}.btn-zoho:hover{background-color:#d63b32}.oauth-icon{font-size:18px}.switch-method{margin-top:12px;text-align:center;font-size:14px}.switch-method a{color:#4285f4;text-decoration:none}.switch-method a:hover{text-decoration:underline}.otp-subtitle{text-align:center;font-size:14px;color:#555;margin-bottom:20px}.otp-digits{display:flex;justify-content:center;gap:8px;margin-bottom:20px}.otp-digit-input{width:44px;height:52px;text-align:center;font-size:22px;font-weight:600;border:2px solid #ddd;border-radius:8px;outline:none;transition:border-color .2s;box-sizing:border-box}.otp-digit-input:focus{border-color:#4285f4}.otp-actions{margin-bottom:12px}.otp-resend{text-align:center;font-size:14px;margin-bottom:8px}.resend-timer{color:#888}.resend-link{color:#4285f4;text-decoration:none;cursor:pointer}.resend-link:hover{text-decoration:underline}.otp-back{text-align:center;font-size:13px}.otp-back a{color:#888;text-decoration:none}.otp-back a:hover{text-decoration:underline;color:#4285f4}.otp-register-section .otp-subtitle{color:#2e7d32}.tenant-list{margin-bottom:20px;display:flex;flex-direction:column;gap:12px}.tenant-item{display:flex;align-items:flex-start;gap:12px;padding:16px;border:2px solid #e0e0e0;border-radius:6px;cursor:pointer;transition:all .2s}.tenant-item:hover{border-color:#4285f4;background:#f8f9ff}.tenant-item.selected{border-color:#4285f4;background:#e8f0fe}.tenant-radio{flex-shrink:0;padding-top:2px}.tenant-radio input[type=radio]{width:18px;height:18px;cursor:pointer}.tenant-info{flex:1}.tenant-name{font-size:16px;font-weight:500;color:#333;margin-bottom:4px}.tenant-meta{font-size:13px;color:#666}.tenant-role{font-weight:500;color:#4285f4}.tenant-separator{margin:0 6px}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.register-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.register-link a{color:#4285f4;text-decoration:none}.register-link a:hover{text-decoration:underline}.create-tenant-link{margin-top:16px;padding-top:16px;border-top:1px solid #e0e0e0;text-align:center;font-size:14px;color:#666}.create-tenant-link a{color:#4285f4;text-decoration:none;font-weight:500}.create-tenant-link a:hover{text-decoration:underline}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i3.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i4.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i4.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i4.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i4.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i4.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i4.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i4.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }] });
|
|
2435
2950
|
}
|
|
2436
2951
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: TenantLoginComponent, decorators: [{
|
|
2437
2952
|
type: Component,
|
|
@@ -2441,8 +2956,119 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
2441
2956
|
<!-- Step 1: Authentication -->
|
|
2442
2957
|
<h2 class="login-title">{{ title }}</h2>
|
|
2443
2958
|
|
|
2959
|
+
<!-- OTP Flow -->
|
|
2960
|
+
@if (isProviderEnabled('otp') && otpActive) {
|
|
2961
|
+
<!-- OTP Step 1: Identifier entry -->
|
|
2962
|
+
@if (otpStep === 'identifier') {
|
|
2963
|
+
<form (ngSubmit)="onOtpSend()" class="otp-form">
|
|
2964
|
+
<div class="form-group">
|
|
2965
|
+
<input
|
|
2966
|
+
[(ngModel)]="otpIdentifier"
|
|
2967
|
+
name="otpIdentifier"
|
|
2968
|
+
placeholder="Enter Email or Phone Number"
|
|
2969
|
+
type="text"
|
|
2970
|
+
required
|
|
2971
|
+
autocomplete="email tel"
|
|
2972
|
+
class="form-control"
|
|
2973
|
+
(input)="onOtpIdentifierChange()">
|
|
2974
|
+
@if (otpIdentifierHint) {
|
|
2975
|
+
<div class="field-hint">{{ otpIdentifierHint }}</div>
|
|
2976
|
+
}
|
|
2977
|
+
</div>
|
|
2978
|
+
<button
|
|
2979
|
+
type="submit"
|
|
2980
|
+
[disabled]="loading || !otpIdentifier.trim()"
|
|
2981
|
+
class="btn btn-primary btn-block">
|
|
2982
|
+
{{ loading ? 'Sending...' : 'Send OTP' }}
|
|
2983
|
+
</button>
|
|
2984
|
+
</form>
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
<!-- OTP Step 2: Code entry -->
|
|
2988
|
+
@if (otpStep === 'code') {
|
|
2989
|
+
<div class="otp-code-section">
|
|
2990
|
+
<p class="otp-subtitle">
|
|
2991
|
+
Enter the 6-digit code sent to
|
|
2992
|
+
<strong>{{ otpMaskedIdentifier }}</strong>
|
|
2993
|
+
</p>
|
|
2994
|
+
<div class="otp-digits">
|
|
2995
|
+
@for (digit of otpDigits; track $index; let i = $index) {
|
|
2996
|
+
<input
|
|
2997
|
+
#otpInput
|
|
2998
|
+
type="text"
|
|
2999
|
+
inputmode="numeric"
|
|
3000
|
+
maxlength="1"
|
|
3001
|
+
class="otp-digit-input"
|
|
3002
|
+
[value]="otpDigits[i]"
|
|
3003
|
+
(input)="onOtpDigitInput($event, i)"
|
|
3004
|
+
(keydown)="onOtpDigitKeydown($event, i)"
|
|
3005
|
+
(paste)="onOtpPaste($event)">
|
|
3006
|
+
}
|
|
3007
|
+
</div>
|
|
3008
|
+
<div class="otp-actions">
|
|
3009
|
+
<button
|
|
3010
|
+
type="button"
|
|
3011
|
+
(click)="onOtpVerify()"
|
|
3012
|
+
[disabled]="loading || otpCode.length < 6"
|
|
3013
|
+
class="btn btn-primary btn-block">
|
|
3014
|
+
{{ loading ? 'Verifying...' : 'Verify' }}
|
|
3015
|
+
</button>
|
|
3016
|
+
</div>
|
|
3017
|
+
<div class="otp-resend">
|
|
3018
|
+
@if (otpResendCountdown > 0) {
|
|
3019
|
+
<span class="resend-timer">
|
|
3020
|
+
Resend in {{ formatCountdown(otpResendCountdown) }}
|
|
3021
|
+
</span>
|
|
3022
|
+
} @else {
|
|
3023
|
+
<a href="#" (click)="onOtpResend($event)" class="resend-link">
|
|
3024
|
+
Resend OTP
|
|
3025
|
+
</a>
|
|
3026
|
+
}
|
|
3027
|
+
</div>
|
|
3028
|
+
<div class="otp-back">
|
|
3029
|
+
<a href="#" (click)="onOtpBack($event)">
|
|
3030
|
+
Use a different email or phone
|
|
3031
|
+
</a>
|
|
3032
|
+
</div>
|
|
3033
|
+
</div>
|
|
3034
|
+
}
|
|
3035
|
+
|
|
3036
|
+
<!-- OTP Step 3: Registration (new user) -->
|
|
3037
|
+
@if (otpStep === 'register') {
|
|
3038
|
+
<div class="otp-register-section">
|
|
3039
|
+
<p class="otp-subtitle">
|
|
3040
|
+
Welcome! Enter your name to get started.
|
|
3041
|
+
</p>
|
|
3042
|
+
<form (ngSubmit)="onOtpRegister()" class="otp-form">
|
|
3043
|
+
<div class="form-group">
|
|
3044
|
+
<input
|
|
3045
|
+
[(ngModel)]="otpDisplayName"
|
|
3046
|
+
name="displayName"
|
|
3047
|
+
placeholder="Your Name"
|
|
3048
|
+
type="text"
|
|
3049
|
+
required
|
|
3050
|
+
class="form-control">
|
|
3051
|
+
</div>
|
|
3052
|
+
<button
|
|
3053
|
+
type="submit"
|
|
3054
|
+
[disabled]="loading || !otpDisplayName.trim()"
|
|
3055
|
+
class="btn btn-primary btn-block">
|
|
3056
|
+
{{ loading ? 'Creating account...' : 'Continue' }}
|
|
3057
|
+
</button>
|
|
3058
|
+
</form>
|
|
3059
|
+
</div>
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
<!-- Divider before other providers -->
|
|
3063
|
+
@if (effectiveOauthProviders.length > 0 || isProviderEnabled('emailPassword')) {
|
|
3064
|
+
<div class="divider">
|
|
3065
|
+
<span>OR</span>
|
|
3066
|
+
</div>
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
|
|
2444
3070
|
<!-- Email/Password Form (if enabled) -->
|
|
2445
|
-
@if (isProviderEnabled('emailPassword') && !useOAuth) {
|
|
3071
|
+
@if (isProviderEnabled('emailPassword') && !useOAuth && !otpActive) {
|
|
2446
3072
|
<form (ngSubmit)="onEmailLogin()" class="email-form">
|
|
2447
3073
|
<div class="form-group">
|
|
2448
3074
|
<input
|
|
@@ -2466,7 +3092,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
2466
3092
|
class="password-toggle"
|
|
2467
3093
|
(click)="showPassword = !showPassword"
|
|
2468
3094
|
[attr.aria-label]="showPassword ? 'Hide password' : 'Show password'">
|
|
2469
|
-
{{ showPassword ? '
|
|
3095
|
+
{{ showPassword ? '👁' : '👁‍🗨' }}
|
|
2470
3096
|
</button>
|
|
2471
3097
|
</div>
|
|
2472
3098
|
<button
|
|
@@ -2478,7 +3104,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
2478
3104
|
</form>
|
|
2479
3105
|
|
|
2480
3106
|
<!-- Divider -->
|
|
2481
|
-
@if (
|
|
3107
|
+
@if (effectiveOauthProviders.length > 0) {
|
|
2482
3108
|
<div class="divider">
|
|
2483
3109
|
<span>OR</span>
|
|
2484
3110
|
</div>
|
|
@@ -2486,9 +3112,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
2486
3112
|
}
|
|
2487
3113
|
|
|
2488
3114
|
<!-- OAuth Providers -->
|
|
2489
|
-
@if (
|
|
3115
|
+
@if (effectiveOauthProviders.length > 0 && !otpActive && (useOAuth || !isProviderEnabled('emailPassword'))) {
|
|
2490
3116
|
<div class="oauth-buttons">
|
|
2491
|
-
@for (provider of
|
|
3117
|
+
@for (provider of effectiveOauthProviders; track provider) {
|
|
2492
3118
|
<button
|
|
2493
3119
|
type="button"
|
|
2494
3120
|
(click)="onOAuthLogin(provider)"
|
|
@@ -2506,7 +3132,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
2506
3132
|
</div>
|
|
2507
3133
|
|
|
2508
3134
|
<!-- Switch to Email/Password -->
|
|
2509
|
-
@if (isProviderEnabled('emailPassword') &&
|
|
3135
|
+
@if (isProviderEnabled('emailPassword') && effectiveOauthProviders.length > 0) {
|
|
2510
3136
|
<div class="switch-method">
|
|
2511
3137
|
<a href="#" (click)="toggleAuthMethod($event)">
|
|
2512
3138
|
{{ useOAuth ? 'Use email/password instead' : 'Use OAuth instead' }}
|
|
@@ -2601,8 +3227,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
2601
3227
|
</div>
|
|
2602
3228
|
}
|
|
2603
3229
|
</div>
|
|
2604
|
-
`, styles: [".tenant-login-dialog{padding:24px;max-width:450px;position:relative}.login-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.welcome-message{margin-bottom:16px;padding:12px;background:#e8f5e9;border-radius:4px;text-align:center;font-size:14px;color:#2e7d32}.selector-description{margin-bottom:20px;font-size:14px;color:#666;text-align:center}.email-form,.form-group{margin-bottom:16px}.password-group{position:relative}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.password-input{padding-right:45px}.password-toggle{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;font-size:18px;padding:8px;line-height:1;opacity:.6;transition:opacity .2s}.password-toggle:hover{opacity:1}.password-toggle:focus{outline:2px solid #4285f4;outline-offset:2px;border-radius:4px}.form-control:focus{outline:none;border-color:#4285f4}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.divider{margin:16px 0;text-align:center;position:relative}.divider:before{content:\"\";position:absolute;top:50%;left:0;right:0;height:1px;background:#ddd}.divider span{background:#fff;padding:0 12px;position:relative;color:#666;font-size:12px}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.btn-zoho{background-color:#f0483e;color:#fff;border:1px solid #d63b32}.btn-zoho:hover{background-color:#d63b32}.oauth-icon{font-size:18px}.switch-method{margin-top:12px;text-align:center;font-size:14px}.switch-method a{color:#4285f4;text-decoration:none}.switch-method a:hover{text-decoration:underline}.tenant-list{margin-bottom:20px;display:flex;flex-direction:column;gap:12px}.tenant-item{display:flex;align-items:flex-start;gap:12px;padding:16px;border:2px solid #e0e0e0;border-radius:6px;cursor:pointer;transition:all .2s}.tenant-item:hover{border-color:#4285f4;background:#f8f9ff}.tenant-item.selected{border-color:#4285f4;background:#e8f0fe}.tenant-radio{flex-shrink:0;padding-top:2px}.tenant-radio input[type=radio]{width:18px;height:18px;cursor:pointer}.tenant-info{flex:1}.tenant-name{font-size:16px;font-weight:500;color:#333;margin-bottom:4px}.tenant-meta{font-size:13px;color:#666}.tenant-role{font-weight:500;color:#4285f4}.tenant-separator{margin:0 6px}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.register-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.register-link a{color:#4285f4;text-decoration:none}.register-link a:hover{text-decoration:underline}.create-tenant-link{margin-top:16px;padding-top:16px;border-top:1px solid #e0e0e0;text-align:center;font-size:14px;color:#666}.create-tenant-link a{color:#4285f4;text-decoration:none;font-weight:500}.create-tenant-link a:hover{text-decoration:underline}\n"] }]
|
|
2605
|
-
}], ctorParameters: () => [{ type: AuthService }, { type: ProviderRegistryService }], propDecorators: {
|
|
3230
|
+
`, styles: [".tenant-login-dialog{padding:24px;max-width:450px;position:relative}.login-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.welcome-message{margin-bottom:16px;padding:12px;background:#e8f5e9;border-radius:4px;text-align:center;font-size:14px;color:#2e7d32}.selector-description{margin-bottom:20px;font-size:14px;color:#666;text-align:center}.email-form,.otp-form,.form-group{margin-bottom:16px}.password-group{position:relative}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.password-input{padding-right:45px}.password-toggle{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;font-size:18px;padding:8px;line-height:1;opacity:.6;transition:opacity .2s}.password-toggle:hover{opacity:1}.password-toggle:focus{outline:2px solid #4285f4;outline-offset:2px;border-radius:4px}.form-control:focus{outline:none;border-color:#4285f4}.field-hint{margin-top:4px;font-size:12px;color:#888}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.divider{margin:16px 0;text-align:center;position:relative}.divider:before{content:\"\";position:absolute;top:50%;left:0;right:0;height:1px;background:#ddd}.divider span{background:#fff;padding:0 12px;position:relative;color:#666;font-size:12px}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.btn-zoho{background-color:#f0483e;color:#fff;border:1px solid #d63b32}.btn-zoho:hover{background-color:#d63b32}.oauth-icon{font-size:18px}.switch-method{margin-top:12px;text-align:center;font-size:14px}.switch-method a{color:#4285f4;text-decoration:none}.switch-method a:hover{text-decoration:underline}.otp-subtitle{text-align:center;font-size:14px;color:#555;margin-bottom:20px}.otp-digits{display:flex;justify-content:center;gap:8px;margin-bottom:20px}.otp-digit-input{width:44px;height:52px;text-align:center;font-size:22px;font-weight:600;border:2px solid #ddd;border-radius:8px;outline:none;transition:border-color .2s;box-sizing:border-box}.otp-digit-input:focus{border-color:#4285f4}.otp-actions{margin-bottom:12px}.otp-resend{text-align:center;font-size:14px;margin-bottom:8px}.resend-timer{color:#888}.resend-link{color:#4285f4;text-decoration:none;cursor:pointer}.resend-link:hover{text-decoration:underline}.otp-back{text-align:center;font-size:13px}.otp-back a{color:#888;text-decoration:none}.otp-back a:hover{text-decoration:underline;color:#4285f4}.otp-register-section .otp-subtitle{color:#2e7d32}.tenant-list{margin-bottom:20px;display:flex;flex-direction:column;gap:12px}.tenant-item{display:flex;align-items:flex-start;gap:12px;padding:16px;border:2px solid #e0e0e0;border-radius:6px;cursor:pointer;transition:all .2s}.tenant-item:hover{border-color:#4285f4;background:#f8f9ff}.tenant-item.selected{border-color:#4285f4;background:#e8f0fe}.tenant-radio{flex-shrink:0;padding-top:2px}.tenant-radio input[type=radio]{width:18px;height:18px;cursor:pointer}.tenant-info{flex:1}.tenant-name{font-size:16px;font-weight:500;color:#333;margin-bottom:4px}.tenant-meta{font-size:13px;color:#666}.tenant-role{font-weight:500;color:#4285f4}.tenant-separator{margin:0 6px}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.register-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.register-link a{color:#4285f4;text-decoration:none}.register-link a:hover{text-decoration:underline}.create-tenant-link{margin-top:16px;padding-top:16px;border-top:1px solid #e0e0e0;text-align:center;font-size:14px;color:#666}.create-tenant-link a{color:#4285f4;text-decoration:none;font-weight:500}.create-tenant-link a:hover{text-decoration:underline}\n"] }]
|
|
3231
|
+
}], ctorParameters: () => [{ type: AuthService }, { type: ProviderRegistryService }], propDecorators: { otpInputs: [{
|
|
3232
|
+
type: ViewChildren,
|
|
3233
|
+
args: ['otpInput']
|
|
3234
|
+
}], title: [{
|
|
2606
3235
|
type: Input
|
|
2607
3236
|
}], providers: [{
|
|
2608
3237
|
type: Input
|
|
@@ -3406,8 +4035,8 @@ class MonthYearPickerComponent {
|
|
|
3406
4035
|
isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : []));
|
|
3407
4036
|
tempMonth = signal(null, ...(ngDevMode ? [{ debugName: "tempMonth" }] : []));
|
|
3408
4037
|
tempYear = signal(null, ...(ngDevMode ? [{ debugName: "tempYear" }] : []));
|
|
3409
|
-
yearStart = signal(this.today.year -
|
|
3410
|
-
years = computed(() => Array.from({ length:
|
|
4038
|
+
yearStart = signal(this.today.year - 3, ...(ngDevMode ? [{ debugName: "yearStart" }] : []));
|
|
4039
|
+
years = computed(() => Array.from({ length: 10 }, (_, i) => this.yearStart() + i), ...(ngDevMode ? [{ debugName: "years" }] : []));
|
|
3411
4040
|
displayValue = computed(() => {
|
|
3412
4041
|
const v = this.value();
|
|
3413
4042
|
return v ? `${this.MONTHS[v.month]} ${v.year}` : '— / —';
|
|
@@ -3468,11 +4097,12 @@ class MonthYearPickerComponent {
|
|
|
3468
4097
|
<span class="font-monospace fw-semibold">{{ preview() }}</span>
|
|
3469
4098
|
</div>
|
|
3470
4099
|
|
|
3471
|
-
<div class="
|
|
3472
|
-
|
|
3473
|
-
<div class="
|
|
3474
|
-
|
|
3475
|
-
|
|
4100
|
+
<div class="nsx-myp-body">
|
|
4101
|
+
<!-- Months: 6 rows × 2 cols -->
|
|
4102
|
+
<div class="nsx-myp-section">
|
|
4103
|
+
<div class="text-uppercase text-muted px-1 mb-1" style="font-size: 10px; letter-spacing: .08em;">Month</div>
|
|
4104
|
+
<div class="nsx-myp-grid nsx-myp-grid-2">
|
|
4105
|
+
@for (month of MONTHS; track $index) {
|
|
3476
4106
|
<button
|
|
3477
4107
|
type="button"
|
|
3478
4108
|
class="btn btn-sm w-100"
|
|
@@ -3482,16 +4112,15 @@ class MonthYearPickerComponent {
|
|
|
3482
4112
|
(click)="selectMonth($index)">
|
|
3483
4113
|
{{ month }}
|
|
3484
4114
|
</button>
|
|
3485
|
-
|
|
3486
|
-
|
|
4115
|
+
}
|
|
4116
|
+
</div>
|
|
3487
4117
|
</div>
|
|
3488
|
-
</div>
|
|
3489
4118
|
|
|
3490
|
-
|
|
3491
|
-
<div class="
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
4119
|
+
<!-- Years: 5 rows × 2 cols + nav row -->
|
|
4120
|
+
<div class="nsx-myp-section">
|
|
4121
|
+
<div class="text-uppercase text-muted px-1 mb-1" style="font-size: 10px; letter-spacing: .08em;">Year</div>
|
|
4122
|
+
<div class="nsx-myp-grid nsx-myp-grid-2">
|
|
4123
|
+
@for (year of years(); track year) {
|
|
3495
4124
|
<button
|
|
3496
4125
|
type="button"
|
|
3497
4126
|
class="btn btn-sm w-100 font-monospace"
|
|
@@ -3501,19 +4130,12 @@ class MonthYearPickerComponent {
|
|
|
3501
4130
|
(click)="selectYear(year)">
|
|
3502
4131
|
{{ year }}
|
|
3503
4132
|
</button>
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
<div class="row g-1 mt-1">
|
|
3509
|
-
<div class="col">
|
|
3510
|
-
<button type="button" class="btn btn-sm btn-light w-100" (click)="shiftYears(-12)">
|
|
3511
|
-
‹ Earlier
|
|
4133
|
+
}
|
|
4134
|
+
<button type="button" class="btn btn-sm btn-light w-100" (click)="shiftYears(-10)">
|
|
4135
|
+
‹
|
|
3512
4136
|
</button>
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
<button type="button" class="btn btn-sm btn-light w-100" (click)="shiftYears(12)">
|
|
3516
|
-
Later ›
|
|
4137
|
+
<button type="button" class="btn btn-sm btn-light w-100" (click)="shiftYears(10)">
|
|
4138
|
+
›
|
|
3517
4139
|
</button>
|
|
3518
4140
|
</div>
|
|
3519
4141
|
</div>
|
|
@@ -3523,24 +4145,24 @@ class MonthYearPickerComponent {
|
|
|
3523
4145
|
<div class="col border-end">
|
|
3524
4146
|
<button
|
|
3525
4147
|
type="button"
|
|
3526
|
-
class="btn w-100 rounded-0 py-2
|
|
3527
|
-
(click)="
|
|
3528
|
-
|
|
4148
|
+
class="btn btn-light w-100 rounded-0 py-2"
|
|
4149
|
+
(click)="cancel()">
|
|
4150
|
+
Cancel
|
|
3529
4151
|
</button>
|
|
3530
4152
|
</div>
|
|
3531
4153
|
<div class="col">
|
|
3532
4154
|
<button
|
|
3533
4155
|
type="button"
|
|
3534
|
-
class="btn
|
|
3535
|
-
(click)="
|
|
3536
|
-
|
|
4156
|
+
class="btn w-100 rounded-0 py-2 fw-semibold text-success"
|
|
4157
|
+
(click)="confirm()">
|
|
4158
|
+
OK
|
|
3537
4159
|
</button>
|
|
3538
4160
|
</div>
|
|
3539
4161
|
</div>
|
|
3540
4162
|
|
|
3541
4163
|
</div>
|
|
3542
4164
|
}
|
|
3543
|
-
`, isInline: true, styles: [":host{position:relative;display:block}.nsx-myp-backdrop{position:fixed;inset:0;z-index:1050}.nsx-myp-popup{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);z-index:1051;background:#fff;width:
|
|
4165
|
+
`, isInline: true, styles: [":host{position:relative;display:block}.nsx-myp-backdrop{position:fixed;inset:0;z-index:1050}.nsx-myp-popup{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);z-index:1051;background:#fff;width:340px;max-width:92vw;overflow:hidden}.nsx-myp-body{display:flex;gap:.5rem;padding:.5rem}.nsx-myp-section{flex:1;min-width:0}.nsx-myp-grid{display:grid;gap:3px}.nsx-myp-grid-2{grid-template-columns:1fr 1fr}\n"] });
|
|
3544
4166
|
}
|
|
3545
4167
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: MonthYearPickerComponent, decorators: [{
|
|
3546
4168
|
type: Component,
|
|
@@ -3566,11 +4188,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
3566
4188
|
<span class="font-monospace fw-semibold">{{ preview() }}</span>
|
|
3567
4189
|
</div>
|
|
3568
4190
|
|
|
3569
|
-
<div class="
|
|
3570
|
-
|
|
3571
|
-
<div class="
|
|
3572
|
-
|
|
3573
|
-
|
|
4191
|
+
<div class="nsx-myp-body">
|
|
4192
|
+
<!-- Months: 6 rows × 2 cols -->
|
|
4193
|
+
<div class="nsx-myp-section">
|
|
4194
|
+
<div class="text-uppercase text-muted px-1 mb-1" style="font-size: 10px; letter-spacing: .08em;">Month</div>
|
|
4195
|
+
<div class="nsx-myp-grid nsx-myp-grid-2">
|
|
4196
|
+
@for (month of MONTHS; track $index) {
|
|
3574
4197
|
<button
|
|
3575
4198
|
type="button"
|
|
3576
4199
|
class="btn btn-sm w-100"
|
|
@@ -3580,16 +4203,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
3580
4203
|
(click)="selectMonth($index)">
|
|
3581
4204
|
{{ month }}
|
|
3582
4205
|
</button>
|
|
3583
|
-
|
|
3584
|
-
|
|
4206
|
+
}
|
|
4207
|
+
</div>
|
|
3585
4208
|
</div>
|
|
3586
|
-
</div>
|
|
3587
4209
|
|
|
3588
|
-
|
|
3589
|
-
<div class="
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
4210
|
+
<!-- Years: 5 rows × 2 cols + nav row -->
|
|
4211
|
+
<div class="nsx-myp-section">
|
|
4212
|
+
<div class="text-uppercase text-muted px-1 mb-1" style="font-size: 10px; letter-spacing: .08em;">Year</div>
|
|
4213
|
+
<div class="nsx-myp-grid nsx-myp-grid-2">
|
|
4214
|
+
@for (year of years(); track year) {
|
|
3593
4215
|
<button
|
|
3594
4216
|
type="button"
|
|
3595
4217
|
class="btn btn-sm w-100 font-monospace"
|
|
@@ -3599,19 +4221,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
3599
4221
|
(click)="selectYear(year)">
|
|
3600
4222
|
{{ year }}
|
|
3601
4223
|
</button>
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
<div class="row g-1 mt-1">
|
|
3607
|
-
<div class="col">
|
|
3608
|
-
<button type="button" class="btn btn-sm btn-light w-100" (click)="shiftYears(-12)">
|
|
3609
|
-
‹ Earlier
|
|
4224
|
+
}
|
|
4225
|
+
<button type="button" class="btn btn-sm btn-light w-100" (click)="shiftYears(-10)">
|
|
4226
|
+
‹
|
|
3610
4227
|
</button>
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
<button type="button" class="btn btn-sm btn-light w-100" (click)="shiftYears(12)">
|
|
3614
|
-
Later ›
|
|
4228
|
+
<button type="button" class="btn btn-sm btn-light w-100" (click)="shiftYears(10)">
|
|
4229
|
+
›
|
|
3615
4230
|
</button>
|
|
3616
4231
|
</div>
|
|
3617
4232
|
</div>
|
|
@@ -3621,24 +4236,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
3621
4236
|
<div class="col border-end">
|
|
3622
4237
|
<button
|
|
3623
4238
|
type="button"
|
|
3624
|
-
class="btn w-100 rounded-0 py-2
|
|
3625
|
-
(click)="
|
|
3626
|
-
|
|
4239
|
+
class="btn btn-light w-100 rounded-0 py-2"
|
|
4240
|
+
(click)="cancel()">
|
|
4241
|
+
Cancel
|
|
3627
4242
|
</button>
|
|
3628
4243
|
</div>
|
|
3629
4244
|
<div class="col">
|
|
3630
4245
|
<button
|
|
3631
4246
|
type="button"
|
|
3632
|
-
class="btn
|
|
3633
|
-
(click)="
|
|
3634
|
-
|
|
4247
|
+
class="btn w-100 rounded-0 py-2 fw-semibold text-success"
|
|
4248
|
+
(click)="confirm()">
|
|
4249
|
+
OK
|
|
3635
4250
|
</button>
|
|
3636
4251
|
</div>
|
|
3637
4252
|
</div>
|
|
3638
4253
|
|
|
3639
4254
|
</div>
|
|
3640
4255
|
}
|
|
3641
|
-
`, styles: [":host{position:relative;display:block}.nsx-myp-backdrop{position:fixed;inset:0;z-index:1050}.nsx-myp-popup{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);z-index:1051;background:#fff;width:
|
|
4256
|
+
`, styles: [":host{position:relative;display:block}.nsx-myp-backdrop{position:fixed;inset:0;z-index:1050}.nsx-myp-popup{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);z-index:1051;background:#fff;width:340px;max-width:92vw;overflow:hidden}.nsx-myp-body{display:flex;gap:.5rem;padding:.5rem}.nsx-myp-section{flex:1;min-width:0}.nsx-myp-grid{display:grid;gap:3px}.nsx-myp-grid-2{grid-template-columns:1fr 1fr}\n"] }]
|
|
3642
4257
|
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }] } });
|
|
3643
4258
|
|
|
3644
4259
|
class TenantRegisterComponent {
|