From e5610d9673b27694a880fdb8b4ff52c315821239 Mon Sep 17 00:00:00 2001 From: prakharlowanshi Date: Mon, 20 Jul 2026 18:44:42 +0530 Subject: [PATCH 1/3] test users implementation --- .../management/management.component.html | 120 ++++++++++++++++- .../users/management/management.component.ts | 125 +++++++++++++++++- 2 files changed, 243 insertions(+), 2 deletions(-) diff --git a/apps/36-blocks/src/app/panel/users/management/management.component.html b/apps/36-blocks/src/app/panel/users/management/management.component.html index 02e717ef..da569ef7 100644 --- a/apps/36-blocks/src/app/panel/users/management/management.component.html +++ b/apps/36-blocks/src/app/panel/users/management/management.component.html @@ -281,8 +281,85 @@ } - + @if (selectedSectionIndex === 3) { +
+
+

Test Users

+

+ Users who log in with a matching email or mobile are treated as test users. On logout, + their account (and any organization where they are the only member) is deleted. +

+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Email + {{ element.email || '—' }} + Mobile + {{ element.mobile || '—' }} + +
+ +
+
+ +
+
+ } + + + @if (selectedSectionIndex === 4) {

Default Roles

@@ -400,3 +477,44 @@

{{ isEditPermissionMode ? 'Edit Permission' : 'Add Permission + + + +

Add Test User

+ + + + Email + + @if (dialogTestUserForm.get('email')?.hasError('email')) { + Enter a valid email + } + + + + Mobile + + Include the country code with a leading + (e.g. +919876543210) + @if (dialogTestUserForm.get('mobile')?.hasError('maxlength')) { + Mobile must be 20 characters or fewer + } + + + @if ( + dialogTestUserForm.hasError('atLeastOneContact') && + (dialogTestUserForm.dirty || dialogTestUserForm.touched) + ) { +

Provide at least one of email or mobile.

+ } + +

+ Warning: adding a real user's email or mobile will delete their account (and any organization where they + are the only member) on their next logout. +

+ +
+ + + + +
diff --git a/apps/36-blocks/src/app/panel/users/management/management.component.ts b/apps/36-blocks/src/app/panel/users/management/management.component.ts index e0d0db32..0d351cb4 100644 --- a/apps/36-blocks/src/app/panel/users/management/management.component.ts +++ b/apps/36-blocks/src/app/panel/users/management/management.component.ts @@ -32,7 +32,7 @@ import { MatPaginatorGotoComponent } from '@proxy/ui/mat-paginator-goto'; import { ConfirmDialogComponent } from '@proxy/ui/confirm-dialog'; import { CopyButtonComponent } from '@proxy/ui/copy-button'; import { MarkdownModule } from 'ngx-markdown'; -import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { AbstractControl, FormControl, FormGroup, ValidationErrors, Validators } from '@angular/forms'; import { MatTableDataSource } from '@angular/material/table'; import { PageEvent } from '@angular/material/paginator'; import { FeatureComponentStore } from '../../features/feature/feature.store'; @@ -55,6 +55,11 @@ interface IRole { description?: string; } +interface ITestIdentity { + email?: string; + mobile?: string; +} + @Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'proxy-management', @@ -120,7 +125,13 @@ export class ManagementComponent implements OnInit, OnDestroy, OnChanges { public availablePermissions: any[] = []; public dialogRoleForm: FormGroup; public dialogPermissionForm: FormGroup; + public dialogTestUserForm: FormGroup; public defaultRolesForm: FormGroup; + /** Backend caps test_identities at 50 entries. */ + public readonly maxTestIdentities = 50; + public testIdentities: ITestIdentity[] = []; + public testIdentitiesDisplayedColumns: string[] = ['email', 'mobile', 'actions']; + public testIdentitiesDataSource = new MatTableDataSource([]); public roleCards: { icon: string; title: string; @@ -172,6 +183,7 @@ export class ManagementComponent implements OnInit, OnDestroy, OnChanges { { name: 'Roles', icon: 'person' }, { name: 'Permissions', icon: 'shield' }, { name: 'Snippet', icon: 'code' }, + { name: 'Testing Users', icon: 'person_add' }, { name: 'Settings', icon: 'settings' }, ]; public features$: Observable> = this.featureComponentStore.feature$; @@ -200,6 +212,7 @@ export class ManagementComponent implements OnInit, OnDestroy, OnChanges { @ViewChild('addRoleDialogTemplate', { static: false }) addRoleDialogTemplate: TemplateRef; @ViewChild('addPermissionDialogTemplate', { static: false }) addPermissionDialogTemplate: TemplateRef; + @ViewChild('addTestUserDialogTemplate', { static: false }) addTestUserDialogTemplate: TemplateRef; constructor() { this.dialogRoleForm = new FormGroup({ @@ -212,6 +225,13 @@ export class ManagementComponent implements OnInit, OnDestroy, OnChanges { permissionName: new FormControl('', [Validators.required]), description: new FormControl('', []), }); + this.dialogTestUserForm = new FormGroup( + { + email: new FormControl('', [Validators.email]), + mobile: new FormControl('', [Validators.maxLength(20)]), + }, + { validators: (group: AbstractControl): ValidationErrors | null => this.atLeastOneContactValidator(group) } + ); this.defaultRolesForm = new FormGroup({ defaultRoleForCreator: new FormControl('', []), defaultRoleForMember: new FormControl('', []), @@ -307,6 +327,7 @@ export class ManagementComponent implements OnInit, OnDestroy, OnChanges { defaultRoleForMember: cRoles.default_member_role, hiddenDefaultRoles: hiddenRoles, }); + this.setTestIdentities(this.featureDetails.extra_configurations?.test_identities); this.cdr.markForCheck(); } }); @@ -377,6 +398,18 @@ export class ManagementComponent implements OnInit, OnDestroy, OnChanges { this.loadPermissions(referenceId, this.permissionSearchTerm); } }); + // Refresh feature details after a successful feature update so cached + // extra_configurations (test_identities, default roles) stay in sync. + this.userComponentStore.createUpdateObject$ + .pipe( + filter((updated) => !!updated), + takeUntil(this.destroy$) + ) + .subscribe(() => { + if (this.featureDetails?.id) { + this.userComponentStore.getFeatureDetails(of(this.featureDetails.id)); + } + }); } private loadRoles(referenceId: string, searchTerm?: string): void { @@ -695,6 +728,96 @@ export class ManagementComponent implements OnInit, OnDestroy, OnChanges { }); } + /** Group validator: a test identity must have at least one of email / mobile. */ + private atLeastOneContactValidator(group: AbstractControl): ValidationErrors | null { + const email = (group.get('email')?.value || '').trim(); + const mobile = (group.get('mobile')?.value || '').trim(); + return email || mobile ? null : { atLeastOneContact: true }; + } + + private setTestIdentities(identities: ITestIdentity[] | undefined | null): void { + this.testIdentities = Array.isArray(identities) ? identities.map((entry) => ({ ...entry })) : []; + this.testIdentitiesDataSource.data = this.testIdentities; + } + + public addTestUser(): void { + if (this.testIdentities.length >= this.maxTestIdentities) { + return; + } + this.dialogTestUserForm.reset({ email: '', mobile: '' }); + this.dialogRef = this.dialog.open(this.addTestUserDialogTemplate, { + panelClass: ['mat-dialog'], + autoFocus: true, + restoreFocus: false, + }); + this.dialogRef.afterClosed().subscribe((entry: ITestIdentity | false) => { + if (entry) { + this.setTestIdentities([...this.testIdentities, entry]); + this.persistTestIdentities(); + } + }); + } + + public submitTestUserDialog(): void { + if (!this.dialogTestUserForm.valid) { + return; + } + const { email, mobile } = this.dialogTestUserForm.value; + const entry: ITestIdentity = {}; + const trimmedEmail = (email || '').trim(); + const trimmedMobile = (mobile || '').trim(); + if (trimmedEmail) { + entry.email = trimmedEmail; + } + if (trimmedMobile) { + entry.mobile = trimmedMobile; + } + this.dialogRef.close(entry); + } + + public closeTestUserDialog(): void { + this.dialogRef.close(false); + } + + public removeTestUser(entry: ITestIdentity): void { + const confirmDialogRef: MatDialogRef = this.dialog.open(ConfirmDialogComponent, { + panelClass: ['mat-dialog'], + }); + const label = entry.email || entry.mobile || 'this test user'; + confirmDialogRef.componentRef.setInput( + 'confirmationMessage', + `Are you sure you want to remove "${label}" from test users?` + ); + confirmDialogRef.componentRef.setInput('confirmButtonText', 'Remove'); + confirmDialogRef.componentRef.setInput('confirmButtonColor', 'warn'); + + confirmDialogRef.afterClosed().subscribe((action) => { + if (action === 'yes') { + this.setTestIdentities(this.testIdentities.filter((item) => item !== entry)); + this.persistTestIdentities(); + } + }); + } + + /** + * Persists the FULL test_identities list. The backend merges list values by + * index, so the complete desired array must be sent on every change. + */ + private persistTestIdentities(): void { + if (!this.featureDetails?.id) { + return; + } + const payload = { + id: this.featureDetails.id, + body: { + extra_configurations: { + test_identities: this.testIdentities, + }, + }, + }; + this.userComponentStore.updateFeature(of(payload)); + } + ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); From 36987487a066ca43d62d39ea5aa94435ce3f2fc1 Mon Sep 17 00:00:00 2001 From: prakharlowanshi Date: Tue, 21 Jul 2026 13:06:30 +0530 Subject: [PATCH 2/3] fix the sub thread issue --- .../src/app/panel/layout/layout.component.ts | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/apps/36-blocks/src/app/panel/layout/layout.component.ts b/apps/36-blocks/src/app/panel/layout/layout.component.ts index 887966e1..5c5f5803 100644 --- a/apps/36-blocks/src/app/panel/layout/layout.component.ts +++ b/apps/36-blocks/src/app/panel/layout/layout.component.ts @@ -151,27 +151,27 @@ export class LayoutComponent extends BaseComponent implements OnInit, OnDestroy } }); - this.clientSettings$.pipe(takeUntil(this.destroy$)).subscribe((clientSettings) => { - if (clientSettings?.client) { - this.logInData$.pipe(take(1)).subscribe((existingLoginData) => { - if (!existingLoginData) { - this.store.dispatch( - logInActions.authenticatedAction({ - response: { - uid: String(clientSettings.client.id), - displayName: clientSettings.client.name, - email: clientSettings.client.email, - phoneNumber: clientSettings.client.mobile ?? null, - photoURL: null, - emailVerified: false, - jwtToken: this.authService.getTokenSync(), - }, - }) - ); - } - }); - } - }); + // this.clientSettings$.pipe(takeUntil(this.destroy$)).subscribe((clientSettings) => { + // if (clientSettings?.client) { + // this.logInData$.pipe(take(1)).subscribe((existingLoginData) => { + // if (!existingLoginData) { + // this.store.dispatch( + // logInActions.authenticatedAction({ + // response: { + // uid: String(clientSettings.client.id), + // displayName: clientSettings.client.name, + // email: clientSettings.client.email, + // phoneNumber: clientSettings.client.mobile ?? null, + // photoURL: null, + // emailVerified: false, + // jwtToken: this.authService.getTokenSync(), + // }, + // }) + // ); + // } + // }); + // } + // }); combineLatest([this.logInData$, this.clientSettings$]).subscribe(([loginData, clientSettings]) => { if (loginData && clientSettings) { From bd7dc18e7257cd7005c2faab4f892f5eead92280 Mon Sep 17 00:00:00 2001 From: prakharlowanshi Date: Wed, 22 Jul 2026 16:13:05 +0530 Subject: [PATCH 3/3] fix the branding issue --- .../36-blocks-widget/src/app/app.component.ts | 7 +- .../src/app/otp/widget/widget.component.ts | 5 +- .../create-feature.component.html | 67 +++++++++++-------- .../create-feature.component.ts | 2 + 4 files changed, 47 insertions(+), 34 deletions(-) diff --git a/apps/36-blocks-widget/src/app/app.component.ts b/apps/36-blocks-widget/src/app/app.component.ts index 65b222f5..9a2aefc7 100644 --- a/apps/36-blocks-widget/src/app/app.component.ts +++ b/apps/36-blocks-widget/src/app/app.component.ts @@ -5,12 +5,11 @@ import { BaseComponent } from '@proxy/ui/base-component'; import { WidgetTheme, PublicScriptType, WidgetConfig, PROXY_DOM_ID } from '@proxy/constant'; import { WidgetThemeService } from './otp/service/widget-theme.service'; -const REFERENCE_ID = '4512365b177668815069e61c1692def'; +const REFERENCE_ID = '4512365o17835822266a4f4e12e0cd1'; const THEME: WidgetTheme = WidgetTheme.System; -const TYPE: PublicScriptType = PublicScriptType.UserProfile; +const TYPE: PublicScriptType = PublicScriptType.Authorization; const OPEN_EDIT_PROFILE = true; -const AUTH_TOKEN = - 'VnNwWTNwWEZYV2VicU1mWElaNjU2S08vZS94N3JxVVhCcUt2RU9KVy9JZ1BlWEJPczlTeU1ZNlowRVRrbVFPVGNIT09XNjJRelB3VXg0V0kyR1NGSEc3Q1VjRTNKVGJrVnk4WWMwdWJWYTBrUGJYYSs4UXpqdVkwOGVnOHI4Sk1DRW5lZDdxODFTTk14SC9UczVFTGJReWZyTmg4NHRKZVBOTFRPdzlMS2xOb29IcmZZWkhTd0FwUnBoRTY5TjJJOU1VbUdKeEhRdzRhTHFlSFpxaTByQT09'; +const AUTH_TOKEN = ''; @Component({ selector: 'proxy-root', diff --git a/apps/36-blocks-widget/src/app/otp/widget/widget.component.ts b/apps/36-blocks-widget/src/app/otp/widget/widget.component.ts index 033297fc..0e9135c5 100644 --- a/apps/36-blocks-widget/src/app/otp/widget/widget.component.ts +++ b/apps/36-blocks-widget/src/app/otp/widget/widget.component.ts @@ -1499,7 +1499,10 @@ export class ProxyAuthWidgetComponent extends BaseComponent implements OnInit, O this.renderer.appendChild(paragraph, link); this.renderer.appendChild(element, paragraph); - // Powered by footer + // Powered by footer — hidden when branding is removed (ui_preferences.remove_branding) + if (selectWidgetTheme?.ui_preferences?.remove_branding) { + return; + } const poweredBy: HTMLParagraphElement = this.renderer.createElement('a'); poweredBy.setAttribute('href', 'https://36blocks.com'); poweredBy.setAttribute('target', '_blank'); diff --git a/apps/36-blocks/src/app/panel/features/create-feature/create-feature.component.html b/apps/36-blocks/src/app/panel/features/create-feature/create-feature.component.html index e1086b39..ddbcbaef 100644 --- a/apps/36-blocks/src/app/panel/features/create-feature/create-feature.component.html +++ b/apps/36-blocks/src/app/panel/features/create-feature/create-feature.component.html @@ -919,35 +919,37 @@

- Powered by - - - - - - - + @if (!featureForm.get('brandingDetails.remove_branding')?.value) { + + Powered by + + + + + + + + }

@@ -2262,6 +2264,13 @@
Taxes
color="primary" > +
+ Remove branding + +
@if (isEditMode) {
Block new user sign-ups diff --git a/apps/36-blocks/src/app/panel/features/create-feature/create-feature.component.ts b/apps/36-blocks/src/app/panel/features/create-feature/create-feature.component.ts index 4e1c4d4a..cd0061c4 100644 --- a/apps/36-blocks/src/app/panel/features/create-feature/create-feature.component.ts +++ b/apps/36-blocks/src/app/panel/features/create-feature/create-feature.component.ts @@ -345,6 +345,7 @@ export class CreateFeatureComponent extends BaseComponent implements OnDestroy, title: new FormControl('Sign in to your account', []), sign_up_button_text: new FormControl('Create an account', [Validators.required]), create_account_link: new FormControl(true, []), + remove_branding: new FormControl(false, []), theme: new FormControl('system', []), version: new FormControl('v2', []), }), @@ -459,6 +460,7 @@ export class CreateFeatureComponent extends BaseComponent implements OnDestroy, sign_up_button_text: feature.ui_preferences?.sign_up_button_text ?? 'Create an account', icons: feature.ui_preferences?.icons ?? false, create_account_link: feature.block_registration ?? true, + remove_branding: feature.ui_preferences?.remove_branding ?? false, theme: feature.ui_preferences?.theme || 'system', version: feature.ui_preferences?.version || 'v1', },