@progalaxyelabs/ngx-stonescriptphp-client 1.22.1 → 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
- this.oauthProviders = this.providers.filter(p => p !== 'emailPassword');
2050
- // If only emailPassword is available, use it by default
2051
- if (this.oauthProviders.length === 0 && this.isProviderEnabled('emailPassword')) {
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; // Switch to email/password form
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 ? '&#x1F441;' : '&#x1F441;&#x200D;&#x1F5E8;' }}
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 (oauthProviders.length > 0) {
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 (oauthProviders.length > 0 && (useOAuth || !isProviderEnabled('emailPassword'))) {
2834
+ @if (effectiveOauthProviders.length > 0 && !otpActive && (useOAuth || !isProviderEnabled('emailPassword'))) {
2320
2835
  <div class="oauth-buttons">
2321
- @for (provider of oauthProviders; track provider) {
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') && oauthProviders.length > 0) {
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 ? '&#x1F441;' : '&#x1F441;&#x200D;&#x1F5E8;' }}
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 (oauthProviders.length > 0) {
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 (oauthProviders.length > 0 && (useOAuth || !isProviderEnabled('emailPassword'))) {
3115
+ @if (effectiveOauthProviders.length > 0 && !otpActive && (useOAuth || !isProviderEnabled('emailPassword'))) {
2490
3116
  <div class="oauth-buttons">
2491
- @for (provider of oauthProviders; track provider) {
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') && oauthProviders.length > 0) {
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: { title: [{
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