@progalaxyelabs/ngx-stonescriptphp-client 1.24.2 → 1.24.5

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.
@@ -2241,6 +2241,7 @@ class TenantLoginComponent {
2241
2241
  autoSelectSingleTenant = true;
2242
2242
  prefillEmail; // Email to prefill (for account linking flow)
2243
2243
  allowTenantCreation = true;
2244
+ otpIdentifierTypes = ['email', 'phone']; // Allowed OTP identifier types
2244
2245
  // Tenant Selector Labels
2245
2246
  tenantSelectorTitle = 'Select Organization';
2246
2247
  tenantSelectorDescription = 'Choose which organization you want to access:';
@@ -2270,6 +2271,8 @@ class TenantLoginComponent {
2270
2271
  otpStep = 'identifier';
2271
2272
  otpIdentifier = '';
2272
2273
  otpIdentifierHint = '';
2274
+ otpIdentifierError = '';
2275
+ otpIdentifierErrorColor = 'red';
2273
2276
  otpNormalizedIdentifier = ''; // E.164 for phone, as-is for email
2274
2277
  otpMaskedIdentifier = '';
2275
2278
  otpDigits = ['', '', '', '', '', ''];
@@ -2528,12 +2531,12 @@ class TenantLoginComponent {
2528
2531
  async onOtpSend() {
2529
2532
  const raw = this.otpIdentifier.trim();
2530
2533
  if (!raw) {
2531
- this.error = 'Please enter your email or phone number';
2534
+ this.error = `Please enter your ${this.otpIdentifierName}`;
2532
2535
  return;
2533
2536
  }
2534
2537
  const type = this.detectIdentifierType(raw);
2535
2538
  if (!type) {
2536
- this.error = 'Please enter a valid email address or phone number';
2539
+ this.error = `Please enter a valid ${this.otpIdentifierName}`;
2537
2540
  return;
2538
2541
  }
2539
2542
  // Normalize: auto-prepend +91 for 10-digit Indian numbers
@@ -2546,6 +2549,7 @@ class TenantLoginComponent {
2546
2549
  }
2547
2550
  this.loading = true;
2548
2551
  this.error = '';
2552
+ this.otpIdentifierError = '';
2549
2553
  try {
2550
2554
  const result = await this.auth.sendOtp(this.otpNormalizedIdentifier);
2551
2555
  if (!result.success) {
@@ -2566,6 +2570,14 @@ class TenantLoginComponent {
2566
2570
  this.loading = false;
2567
2571
  }
2568
2572
  }
2573
+ /** Skip to OTP code entry when user already has a code */
2574
+ onSkipToOtpEntry() {
2575
+ this.error = '';
2576
+ this.otpMaskedIdentifier = this.otpIdentifier;
2577
+ this.otpDigits = ['', '', '', '', '', ''];
2578
+ this.otpStep = 'code';
2579
+ setTimeout(() => this.focusOtpInput(0), 50);
2580
+ }
2569
2581
  /** Verify the entered OTP code */
2570
2582
  async onOtpVerify() {
2571
2583
  if (this.loading)
@@ -2580,7 +2592,26 @@ class TenantLoginComponent {
2580
2592
  try {
2581
2593
  const verifyResult = await this.auth.verifyOtp(this.otpNormalizedIdentifier, code);
2582
2594
  if (!verifyResult.success) {
2583
- this.error = 'Invalid code. Please try again.';
2595
+ switch (verifyResult.error) {
2596
+ case 'otp_expired':
2597
+ this.resetToIdentifierStep('Your code has expired. Please request a new one.', 'orange');
2598
+ break;
2599
+ case 'otp_invalid': {
2600
+ const attempts = verifyResult.remaining_attempts;
2601
+ this.error = attempts !== undefined
2602
+ ? `Invalid code. ${attempts} attempt${attempts === 1 ? '' : 's'} remaining.`
2603
+ : 'Invalid code. Please try again.';
2604
+ break;
2605
+ }
2606
+ case 'otp_rate_limited':
2607
+ this.resetToIdentifierStep('Too many attempts. Please try again later.', 'red');
2608
+ break;
2609
+ case 'otp_not_found':
2610
+ this.resetToIdentifierStep('No code found. Please request a new one.', 'orange');
2611
+ break;
2612
+ default:
2613
+ this.error = verifyResult.message || 'Invalid code. Please try again.';
2614
+ }
2584
2615
  return;
2585
2616
  }
2586
2617
  this.otpVerifiedToken = verifyResult.verified_token;
@@ -2660,8 +2691,23 @@ class TenantLoginComponent {
2660
2691
  this.otpDigits = ['', '', '', '', '', ''];
2661
2692
  this.otpVerifiedToken = '';
2662
2693
  this.error = '';
2694
+ this.otpIdentifierError = '';
2663
2695
  this.clearResendTimer();
2664
2696
  }
2697
+ /**
2698
+ * Reset to the identifier entry step (step 1) with an error message.
2699
+ * Used when OTP verification fails with a code that requires re-sending
2700
+ * (expired, rate-limited, not-found). Keeps the identifier pre-filled.
2701
+ */
2702
+ resetToIdentifierStep(message, color) {
2703
+ this.otpStep = 'identifier';
2704
+ this.otpDigits = ['', '', '', '', '', ''];
2705
+ this.otpVerifiedToken = '';
2706
+ this.error = '';
2707
+ this.clearResendTimer();
2708
+ this.otpIdentifierError = message;
2709
+ this.otpIdentifierErrorColor = color;
2710
+ }
2665
2711
  // ── OTP digit input handling ─────────────────────────────────────────────
2666
2712
  onOtpDigitInput(event, index) {
2667
2713
  const input = event.target;
@@ -2705,15 +2751,46 @@ class TenantLoginComponent {
2705
2751
  get otpCode() {
2706
2752
  return this.otpDigits.join('');
2707
2753
  }
2754
+ get otpPlaceholderText() {
2755
+ const allowsEmail = this.otpIdentifierTypes.includes('email');
2756
+ const allowsPhone = this.otpIdentifierTypes.includes('phone');
2757
+ if (allowsEmail && allowsPhone) {
2758
+ return 'Enter Email or Phone Number';
2759
+ }
2760
+ else if (allowsEmail) {
2761
+ return 'Enter Email';
2762
+ }
2763
+ else if (allowsPhone) {
2764
+ return 'Enter Phone Number';
2765
+ }
2766
+ return 'Enter Identifier';
2767
+ }
2768
+ get otpIdentifierName() {
2769
+ const allowsEmail = this.otpIdentifierTypes.includes('email');
2770
+ const allowsPhone = this.otpIdentifierTypes.includes('phone');
2771
+ if (allowsEmail && allowsPhone) {
2772
+ return 'email or phone number';
2773
+ }
2774
+ else if (allowsEmail) {
2775
+ return 'email';
2776
+ }
2777
+ else if (allowsPhone) {
2778
+ return 'phone number';
2779
+ }
2780
+ return 'identifier';
2781
+ }
2708
2782
  // ── OTP helpers ──────────────────────────────────────────────────────────
2709
2783
  detectIdentifierType(value) {
2784
+ const allowsEmail = this.otpIdentifierTypes.includes('email');
2785
+ const allowsPhone = this.otpIdentifierTypes.includes('phone');
2710
2786
  if (value.includes('@')) {
2711
2787
  // Basic email validation
2712
- return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? 'email' : null;
2788
+ const isValidEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
2789
+ return (isValidEmail && allowsEmail) ? 'email' : null;
2713
2790
  }
2714
2791
  const digits = value.replace(/\D/g, '');
2715
2792
  if (digits.length >= 10 && digits.length <= 15) {
2716
- return 'phone';
2793
+ return allowsPhone ? 'phone' : null;
2717
2794
  }
2718
2795
  return null;
2719
2796
  }
@@ -2775,7 +2852,7 @@ class TenantLoginComponent {
2775
2852
  this.createTenant.emit();
2776
2853
  }
2777
2854
  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 });
2778
- 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: `
2855
+ 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", otpIdentifierTypes: "otpIdentifierTypes", 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: `
2779
2856
  <div class="tenant-login-dialog">
2780
2857
  @if (!showingTenantSelector) {
2781
2858
  <!-- Step 1: Authentication -->
@@ -2786,11 +2863,16 @@ class TenantLoginComponent {
2786
2863
  <!-- OTP Step 1: Identifier entry -->
2787
2864
  @if (otpStep === 'identifier') {
2788
2865
  <form (ngSubmit)="onOtpSend()" class="otp-form">
2866
+ @if (otpIdentifierError) {
2867
+ <div [class]="'otp-identifier-error otp-identifier-error--' + otpIdentifierErrorColor">
2868
+ {{ otpIdentifierError }}
2869
+ </div>
2870
+ }
2789
2871
  <div class="form-group">
2790
2872
  <input
2791
2873
  [(ngModel)]="otpIdentifier"
2792
2874
  name="otpIdentifier"
2793
- placeholder="Enter Email or Phone Number"
2875
+ [placeholder]="otpPlaceholderText"
2794
2876
  type="text"
2795
2877
  required
2796
2878
  autocomplete="email tel"
@@ -2806,6 +2888,14 @@ class TenantLoginComponent {
2806
2888
  class="btn btn-primary btn-block">
2807
2889
  {{ loading ? 'Sending...' : 'Send OTP' }}
2808
2890
  </button>
2891
+ @if (error && otpIdentifier.trim()) {
2892
+ <button
2893
+ type="button"
2894
+ class="btn btn-link btn-block otp-already-have"
2895
+ (click)="onSkipToOtpEntry()">
2896
+ I already have an OTP
2897
+ </button>
2898
+ }
2809
2899
  </form>
2810
2900
  }
2811
2901
 
@@ -2852,7 +2942,7 @@ class TenantLoginComponent {
2852
2942
  </div>
2853
2943
  <div class="otp-back">
2854
2944
  <a href="#" (click)="onOtpBack($event)">
2855
- Use a different email or phone
2945
+ Use a different {{ otpIdentifierName }}
2856
2946
  </a>
2857
2947
  </div>
2858
2948
  </div>
@@ -3052,7 +3142,7 @@ class TenantLoginComponent {
3052
3142
  </div>
3053
3143
  }
3054
3144
  </div>
3055
- `, 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"] }] });
3145
+ `, 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}.otp-identifier-error{margin-bottom:12px;padding:10px 12px;border-radius:4px;font-size:14px}.otp-identifier-error--red{background:#fee;color:#c33}.otp-identifier-error--orange{background:#fff3e0;color:#e65100}.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"] }] });
3056
3146
  }
3057
3147
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: TenantLoginComponent, decorators: [{
3058
3148
  type: Component,
@@ -3067,11 +3157,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3067
3157
  <!-- OTP Step 1: Identifier entry -->
3068
3158
  @if (otpStep === 'identifier') {
3069
3159
  <form (ngSubmit)="onOtpSend()" class="otp-form">
3160
+ @if (otpIdentifierError) {
3161
+ <div [class]="'otp-identifier-error otp-identifier-error--' + otpIdentifierErrorColor">
3162
+ {{ otpIdentifierError }}
3163
+ </div>
3164
+ }
3070
3165
  <div class="form-group">
3071
3166
  <input
3072
3167
  [(ngModel)]="otpIdentifier"
3073
3168
  name="otpIdentifier"
3074
- placeholder="Enter Email or Phone Number"
3169
+ [placeholder]="otpPlaceholderText"
3075
3170
  type="text"
3076
3171
  required
3077
3172
  autocomplete="email tel"
@@ -3087,6 +3182,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3087
3182
  class="btn btn-primary btn-block">
3088
3183
  {{ loading ? 'Sending...' : 'Send OTP' }}
3089
3184
  </button>
3185
+ @if (error && otpIdentifier.trim()) {
3186
+ <button
3187
+ type="button"
3188
+ class="btn btn-link btn-block otp-already-have"
3189
+ (click)="onSkipToOtpEntry()">
3190
+ I already have an OTP
3191
+ </button>
3192
+ }
3090
3193
  </form>
3091
3194
  }
3092
3195
 
@@ -3133,7 +3236,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3133
3236
  </div>
3134
3237
  <div class="otp-back">
3135
3238
  <a href="#" (click)="onOtpBack($event)">
3136
- Use a different email or phone
3239
+ Use a different {{ otpIdentifierName }}
3137
3240
  </a>
3138
3241
  </div>
3139
3242
  </div>
@@ -3333,7 +3436,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3333
3436
  </div>
3334
3437
  }
3335
3438
  </div>
3336
- `, 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"] }]
3439
+ `, 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}.otp-identifier-error{margin-bottom:12px;padding:10px 12px;border-radius:4px;font-size:14px}.otp-identifier-error--red{background:#fee;color:#c33}.otp-identifier-error--orange{background:#fff3e0;color:#e65100}.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"] }]
3337
3440
  }], ctorParameters: () => [{ type: AuthService }, { type: ProviderRegistryService }], propDecorators: { otpInputs: [{
3338
3441
  type: ViewChildren,
3339
3442
  args: ['otpInput']
@@ -3349,6 +3452,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3349
3452
  type: Input
3350
3453
  }], allowTenantCreation: [{
3351
3454
  type: Input
3455
+ }], otpIdentifierTypes: [{
3456
+ type: Input
3352
3457
  }], tenantSelectorTitle: [{
3353
3458
  type: Input
3354
3459
  }], tenantSelectorDescription: [{
@@ -3817,7 +3922,7 @@ class AuthPageComponent {
3817
3922
  }
3818
3923
  </div>
3819
3924
  </div>
3820
- `, isInline: true, styles: [".auth-container{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px;background:linear-gradient(135deg,#667eea,#764ba2)}.auth-card{background:#fff;border-radius:12px;box-shadow:0 10px 40px #0000001a;padding:40px;width:100%;max-width:480px}.logo{display:block;max-width:200px;max-height:80px;margin:0 auto 24px}.app-name{margin:0 0 12px;font-size:28px;font-weight:600;text-align:center;color:#1a202c}.subtitle{margin:0 0 32px;font-size:16px;text-align:center;color:#718096}:host ::ng-deep .tenant-login-dialog,:host ::ng-deep .register-dialog{padding:0;max-width:none}:host ::ng-deep .login-title,:host ::ng-deep .register-title{display:none}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: TenantLoginComponent, selector: "lib-tenant-login", inputs: ["title", "providers", "showTenantSelector", "autoSelectSingleTenant", "prefillEmail", "allowTenantCreation", "tenantSelectorTitle", "tenantSelectorDescription", "continueButtonText", "registerLinkText", "registerLinkAction", "createTenantLinkText", "createTenantLinkAction"], outputs: ["tenantSelected", "needsOnboarding", "createTenant"] }, { kind: "component", type: RegisterComponent, selector: "lib-register", outputs: ["navigateToLogin"] }] });
3925
+ `, isInline: true, styles: [".auth-container{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px;background:linear-gradient(135deg,#667eea,#764ba2)}.auth-card{background:#fff;border-radius:12px;box-shadow:0 10px 40px #0000001a;padding:40px;width:100%;max-width:480px}.logo{display:block;max-width:200px;max-height:80px;margin:0 auto 24px}.app-name{margin:0 0 12px;font-size:28px;font-weight:600;text-align:center;color:#1a202c}.subtitle{margin:0 0 32px;font-size:16px;text-align:center;color:#718096}:host ::ng-deep .tenant-login-dialog,:host ::ng-deep .register-dialog{padding:0;max-width:none}:host ::ng-deep .login-title,:host ::ng-deep .register-title{display:none}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: TenantLoginComponent, selector: "lib-tenant-login", inputs: ["title", "providers", "showTenantSelector", "autoSelectSingleTenant", "prefillEmail", "allowTenantCreation", "otpIdentifierTypes", "tenantSelectorTitle", "tenantSelectorDescription", "continueButtonText", "registerLinkText", "registerLinkAction", "createTenantLinkText", "createTenantLinkAction"], outputs: ["tenantSelected", "needsOnboarding", "createTenant"] }, { kind: "component", type: RegisterComponent, selector: "lib-register", outputs: ["navigateToLogin"] }] });
3821
3926
  }
3822
3927
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: AuthPageComponent, decorators: [{
3823
3928
  type: Component,
@@ -5094,7 +5199,7 @@ class TenantLoginDialogComponent {
5094
5199
  (createTenant)="onCreateTenant()">
5095
5200
  </lib-tenant-login>
5096
5201
  </div>
5097
- `, isInline: true, styles: [".dialog-wrapper{padding:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: TenantLoginComponent, selector: "lib-tenant-login", inputs: ["title", "providers", "showTenantSelector", "autoSelectSingleTenant", "prefillEmail", "allowTenantCreation", "tenantSelectorTitle", "tenantSelectorDescription", "continueButtonText", "registerLinkText", "registerLinkAction", "createTenantLinkText", "createTenantLinkAction"], outputs: ["tenantSelected", "needsOnboarding", "createTenant"] }] });
5202
+ `, isInline: true, styles: [".dialog-wrapper{padding:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: TenantLoginComponent, selector: "lib-tenant-login", inputs: ["title", "providers", "showTenantSelector", "autoSelectSingleTenant", "prefillEmail", "allowTenantCreation", "otpIdentifierTypes", "tenantSelectorTitle", "tenantSelectorDescription", "continueButtonText", "registerLinkText", "registerLinkAction", "createTenantLinkText", "createTenantLinkAction"], outputs: ["tenantSelected", "needsOnboarding", "createTenant"] }] });
5098
5203
  }
5099
5204
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: TenantLoginDialogComponent, decorators: [{
5100
5205
  type: Component,