diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 00000000..3c3629e6
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1 @@
+node_modules
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 1b82be70..770a1fd0 100755
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -44,7 +44,7 @@ jobs:
- name: Install Node.js & NPM
uses: actions/setup-node@v3
with:
- node-version: "16"
+ node-version: "20"
# Runs npm i
- uses: bahmutov/npm-install@v1
@@ -65,3 +65,16 @@ jobs:
run: npx nx affected --target=build --base='origin/production' --configuration=local
- if: ${{github.event.pull_request.base.ref == 'development'}}
run: npx nx affected --target=build --base='origin/development' --configuration=local
+
+ - name: Check widget bundle size
+ if: always()
+ run: |
+ WIDGET_FILE="apps/36-blocks/src/assets/proxy-auth/proxy-auth.js"
+ if [ -f "$WIDGET_FILE" ]; then
+ SIZE=$(stat -c%s "$WIDGET_FILE")
+ echo "proxy-auth.js size: $((SIZE / 1024)) KB"
+ if [ $SIZE -gt 3145728 ]; then
+ echo "::error::proxy-auth.js exceeds 3 MB ($((SIZE / 1024)) KB) — check for bundle bloat!"
+ exit 1
+ fi
+ fi
diff --git a/.gitignore b/.gitignore
index fb276c6e..53408db3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -50,4 +50,12 @@ Thumbs.db
.angular
# dotenv environment variables file
-.env
\ No newline at end of file
+.env
+
+# Generated environment variable files (created by tools/set-env.js from .env)
+apps/36-blocks/src/environments/env-variables.ts
+apps/36-blocks-widget/src/environments/env-variables.ts
+
+# Nx cache
+.nx/cache
+.nx/workspace-data
\ No newline at end of file
diff --git a/.postcssrc.json b/.postcssrc.json
new file mode 100644
index 00000000..fddc8af8
--- /dev/null
+++ b/.postcssrc.json
@@ -0,0 +1,5 @@
+{
+ "plugins": {
+ "@tailwindcss/postcss": {}
+ }
+}
diff --git a/apps/proxy-auth-element/.eslintrc.json b/apps/36-blocks-widget/.eslintrc.json
similarity index 100%
rename from apps/proxy-auth-element/.eslintrc.json
rename to apps/36-blocks-widget/.eslintrc.json
diff --git a/apps/36-blocks-widget/DESIGN-STRUCTURE.md b/apps/36-blocks-widget/DESIGN-STRUCTURE.md
new file mode 100644
index 00000000..ba559361
--- /dev/null
+++ b/apps/36-blocks-widget/DESIGN-STRUCTURE.md
@@ -0,0 +1,131 @@
+# Widget Design Structure
+
+## Stack
+- **Tailwind CSS v4** — default color palette only (`gray-*`, `blue-*`, `red-*`, etc.). No custom token mapping.
+- **Angular Material v21** — MDC components for forms, dialogs, tables, tabs only. No custom Material theming applied to templates.
+- **No inline `style=` attributes** in templates.
+- **No custom SCSS** in component files — only `:host` display/layout overrides if strictly necessary.
+
+---
+
+## Color Conventions (Tailwind defaults)
+
+| Role | Light | Dark |
+|---|---|---|
+| Page background | `bg-white` | `dark:bg-gray-900` |
+| Card background | `bg-white` | `dark:bg-gray-800` |
+| Card border | `border-gray-200` | `dark:border-gray-700` |
+| Primary text | `text-gray-900` | `dark:text-gray-100` |
+| Secondary / muted text | `text-gray-500` | `dark:text-gray-400` |
+| Label / caption text | `text-gray-400` | `dark:text-gray-500` |
+| Primary action (buttons) | `bg-blue-600 text-white` | same |
+| Primary action hover | `hover:bg-blue-700` | same |
+| Danger | `text-red-600` / `bg-red-600` | same |
+| Success | `text-green-600` | same |
+| Input border | `border-gray-300` | `dark:border-gray-600` |
+| Input background | `bg-white` | `dark:bg-gray-800` |
+| Divider | `border-gray-100` | `dark:border-gray-700` |
+
+> Dark mode is toggled via `[class.dark]` on the root widget `
` using Tailwind's `darkMode: ['class']` config.
+
+---
+
+## Typography Conventions
+
+| Use | Class |
+|---|---|
+| Page / card title | `text-lg font-semibold text-gray-900` |
+| Section heading | `text-sm font-semibold text-gray-700` |
+| Body text | `text-sm text-gray-700` |
+| Muted / caption | `text-xs text-gray-500` |
+| Label above field | `text-xs font-medium text-gray-500 uppercase tracking-wide` |
+| Error message | `text-xs text-red-600` |
+
+---
+
+## Spacing Conventions
+
+| Element | Class |
+|---|---|
+| Card padding | `p-6` |
+| Section gap | `gap-6` |
+| Form field gap | `gap-4` |
+| Button gap in row | `gap-3` |
+| Input height | Angular Material `appearance="outline"` handles this |
+
+---
+
+## Component Map
+
+### 1. `widget-shell` — `widget.component.html`
+The outer wrapper rendered by the Angular Element. Contains:
+- Header bar (logo, title, close button)
+- Router outlet for inner views
+- Overlay/modal container
+
+### 2. `send-otp-center` — `send-otp-center.component.html`
+First screen: enter phone number or email to receive OTP.
+- Input field + send button
+- Social login buttons (optional)
+- "Already have an account" link
+
+### 3. `login` — `login.component.html`
+Email + password form.
+- Input fields (email, password with show/hide toggle)
+- "Forgot password" link
+- Submit button
+- Switch to OTP/register
+
+### 4. `register` — `register.component.html`
+Multi-step registration:
+- Step 1: name, email, phone
+- Step 2: OTP verification (4 boxes)
+- Step 3: password setup
+
+### 5. `subscription-center` — `subscription-center.component.html`
+Plan selection:
+- Plan cards (title, price, features list)
+- CTA button per card
+- Current plan highlight
+
+### 6. `user-profile` — `user-profile.component.html`
+Authenticated user view/edit:
+- Avatar + name + email banner
+- View mode: read-only field rows
+- Edit mode: form fields
+- Organizations list / table
+
+### 7. `user-management` — `user-management.component.html`
+Admin panel:
+- Members tab: search, user rows, edit/remove buttons
+- Roles tab: roles table with permissions
+- Permissions tab
+
+### 8. `organization-details` — `organization-details.component.html`
+Org settings form:
+- Name, timezone, logo upload
+- Save/cancel buttons
+
+---
+
+## Migration Order (tell me which to start)
+1. `send-otp-center`
+2. `login`
+3. `register`
+4. `subscription-center`
+5. `user-profile`
+6. `user-management`
+7. `organization-details`
+8. `widget-shell` (last — depends on inner views)
+
+---
+
+## Files Reference
+
+| File | Purpose |
+|---|---|
+| `src/tailwind-blocks.html` | Paste paid Tailwind UI blocks here |
+| `src/styles.scss` | Global styles — minimal |
+| `src/otp-global.scss` | Widget-specific global overrides |
+| `apps/shared/scss/global.scss` | Tailwind import + shared utilities |
+| `tailwind.config.js` (root) | Tailwind config — content globs + darkMode |
diff --git a/apps/36-blocks-widget/build-elements.js b/apps/36-blocks-widget/build-elements.js
new file mode 100755
index 00000000..34d03fda
--- /dev/null
+++ b/apps/36-blocks-widget/build-elements.js
@@ -0,0 +1,87 @@
+const fs = require('fs-extra');
+const path = require('path');
+
+(async function build() {
+ const distDir = './dist/apps/36-blocks-widget/browser';
+ const outDir = './apps/36-blocks/src/assets/proxy-auth';
+
+ if (!(await fs.pathExists(distDir))) {
+ throw new Error(`Widget dist not found: ${distDir}`);
+ }
+
+ // Dynamic discovery with priority-based ordering — future-proof against Angular output changes
+ const allFiles = await fs.readdir(distDir);
+ const priority = ['polyfills', 'vendor', 'main'];
+ const jsFiles = allFiles
+ .filter((f) => f.endsWith('.js'))
+ .sort((a, b) => {
+ const getPriority = (f) => {
+ const index = priority.findIndex((p) => f.includes(p));
+ return index === -1 ? priority.length : index;
+ };
+ return getPriority(a) - getPriority(b);
+ });
+
+ if (jsFiles.length === 0) {
+ throw new Error(`No JS files found in ${distDir}`);
+ }
+
+ console.info('Concatenating:', jsFiles);
+
+ // Read and concat all JS files in order
+ const contents = [];
+ for (const file of jsFiles) {
+ contents.push(await fs.readFile(path.join(distDir, file), 'utf8'));
+ }
+
+ // Inline styles.css if it exists
+ const stylesPath = path.join(distDir, 'styles.css');
+ if (await fs.pathExists(stylesPath)) {
+ console.info('Inlining styles.css...');
+ const cssContent = await fs.readFile(stylesPath, 'utf8');
+ // Escape backticks and backslashes for JS template literal
+ const escapedCSS = cssContent.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
+
+ // Create a self-executing function that injects styles into document.head
+ const styleInjector = `
+(function() {
+ if (typeof window === 'undefined' || !window.document) return;
+ if (document.getElementById('proxy-auth-widget-styles')) return;
+
+ var style = document.createElement('style');
+ style.id = 'proxy-auth-widget-styles';
+ style.textContent = \`${escapedCSS}\`;
+
+ // Store CSS content globally so widget-portal service can access it
+ if (!window.__proxyAuth) window.__proxyAuth = {};
+ window.__proxyAuth.inlinedStyles = \`${escapedCSS}\`;
+
+ // Inject into document.head
+ (document.head || document.getElementsByTagName('head')[0]).appendChild(style);
+})();
+`;
+ contents.push(styleInjector);
+ } else {
+ console.warn('styles.css not found - skipping CSS inlining');
+ }
+
+ await fs.ensureDir(outDir);
+ const outPath = path.join(outDir, 'proxy-auth.js');
+ await fs.writeFile(outPath, contents.join('\n'));
+
+ // Copy to dist output directory as well
+ const distOutDir = './dist/apps/36-blocks/browser/assets/proxy-auth';
+ await fs.ensureDir(distOutDir);
+ await fs.copyFile(outPath, path.join(distOutDir, 'proxy-auth.js'));
+
+ // Bundle size check — warn if unexpectedly large
+ const stats = await fs.stat(outPath);
+ const sizeMB = (stats.size / 1048576).toFixed(2);
+ console.info(`proxy-auth.js created: ${sizeMB} MB`);
+ console.info(`Copied to: ${distOutDir}/proxy-auth.js`);
+ if (stats.size > 3 * 1048576) {
+ console.warn('WARNING: proxy-auth.js exceeds 3 MB — check for bundle bloat!');
+ }
+
+ console.info('Elements created successfully!');
+})();
diff --git a/apps/36-blocks-widget/project.json b/apps/36-blocks-widget/project.json
new file mode 100644
index 00000000..03728fca
--- /dev/null
+++ b/apps/36-blocks-widget/project.json
@@ -0,0 +1,147 @@
+{
+ "name": "36-blocks-widget",
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "sourceRoot": "apps/36-blocks-widget/src",
+ "prefix": "proxy",
+ "targets": {
+ "set-env": {
+ "executor": "nx:run-commands",
+ "options": {
+ "command": "node tools/set-env --auth"
+ }
+ },
+ "build": {
+ "dependsOn": ["set-env"],
+ "executor": "@angular-devkit/build-angular:application",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/apps/36-blocks-widget",
+ "browser": "apps/36-blocks-widget/src/main.ts",
+ "index": false,
+ "polyfills": [],
+ "tsConfig": "apps/36-blocks-widget/tsconfig.app.json",
+ "inlineStyleLanguage": "scss",
+ "stylePreprocessorOptions": {
+ "includePaths": ["apps/shared/scss", "apps/shared"]
+ },
+ "assets": [
+ "apps/36-blocks-widget/src/favicon.ico",
+ "apps/36-blocks-widget/src/assets",
+ {
+ "glob": "intl-tel-input-custom.css",
+ "input": "apps/shared/assets/utils",
+ "output": "assets/utils"
+ }
+ ],
+ "styles": ["apps/36-blocks-widget/src/styles.scss"],
+ "scripts": [],
+ "outputHashing": "none",
+ "allowedCommonJsDependencies": ["crypto-js", "dayjs"]
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "1.5mb",
+ "maximumError": "3mb"
+ }
+ ],
+ "fileReplacements": [
+ {
+ "replace": "apps/36-blocks-widget/src/environments/environment.ts",
+ "with": "apps/36-blocks-widget/src/environments/environment.prod.ts"
+ }
+ ],
+ "optimization": {
+ "scripts": true,
+ "styles": true,
+ "fonts": true
+ },
+ "namedChunks": false,
+ "extractLicenses": false,
+ "sourceMap": false
+ },
+ "test": {
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "1.5mb",
+ "maximumError": "3mb"
+ }
+ ],
+ "fileReplacements": [
+ {
+ "replace": "apps/36-blocks-widget/src/environments/environment.ts",
+ "with": "apps/36-blocks-widget/src/environments/environment.test.ts"
+ }
+ ],
+ "optimization": true,
+ "extractLicenses": false,
+ "sourceMap": false
+ },
+ "stage": {
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "1.5mb",
+ "maximumError": "3mb"
+ }
+ ],
+ "fileReplacements": [
+ {
+ "replace": "apps/36-blocks-widget/src/environments/environment.ts",
+ "with": "apps/36-blocks-widget/src/environments/environment.stage.ts"
+ }
+ ],
+ "optimization": true,
+ "extractLicenses": false,
+ "sourceMap": false
+ },
+ "development": {
+ "optimization": false,
+ "extractLicenses": false,
+ "sourceMap": true,
+ "index": "apps/36-blocks-widget/src/index.html",
+ "fileReplacements": [
+ {
+ "replace": "apps/36-blocks-widget/src/main.ts",
+ "with": "apps/36-blocks-widget/src/main.dev.ts"
+ }
+ ]
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "serve": {
+ "dependsOn": ["set-env"],
+ "executor": "@angular-devkit/build-angular:dev-server",
+ "options": {
+ "buildTarget": "36-blocks-widget:build:development"
+ },
+ "configurations": {
+ "production": {
+ "buildTarget": "36-blocks-widget:build:production"
+ },
+ "development": {
+ "buildTarget": "36-blocks-widget:build:development"
+ }
+ },
+ "defaultConfiguration": "development"
+ },
+ "extract-i18n": {
+ "executor": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "36-blocks-widget:build"
+ }
+ },
+ "lint": {
+ "executor": "@nx/eslint:lint",
+ "options": {
+ "lintFilePatterns": ["apps/36-blocks-widget/**/*.ts", "apps/36-blocks-widget/**/*.html"]
+ }
+ }
+ },
+ "tags": ["scope:36-blocks-widget", "type:application"]
+}
diff --git a/apps/36-blocks-widget/src/app/app.component.html b/apps/36-blocks-widget/src/app/app.component.html
new file mode 100644
index 00000000..e028f8ec
--- /dev/null
+++ b/apps/36-blocks-widget/src/app/app.component.html
@@ -0,0 +1,2 @@
+
+
diff --git a/apps/36-blocks-widget/src/app/app.component.scss b/apps/36-blocks-widget/src/app/app.component.scss
new file mode 100644
index 00000000..ef6ab2dc
--- /dev/null
+++ b/apps/36-blocks-widget/src/app/app.component.scss
@@ -0,0 +1,41 @@
+// .hcaptcha-container {
+// margin: 20px 0;
+// padding: 15px;
+// border: 1px solid #e0e0e0;
+// border-radius: 8px;
+// background-color: #f9f9f9;
+// display: flex;
+// justify-content: center;
+// }
+
+// .captcha-status {
+// margin: 15px 0;
+// padding: 10px;
+// background-color: #e8f5e8;
+// border: 1px solid #4caf50;
+// border-radius: 4px;
+
+// p {
+// margin: 0 0 10px 0;
+// color: #2e7d32;
+// font-weight: 500;
+// }
+
+// button {
+// background-color: #f44336;
+// color: white;
+// border: none;
+// padding: 8px 16px;
+// border-radius: 4px;
+// cursor: pointer;
+// font-size: 14px;
+
+// &:hover {
+// background-color: #d32f2f;
+// }
+// }
+// }
+
+// // :host div {
+// // height: 100vh;
+// // }
diff --git a/apps/36-blocks-widget/src/app/app.component.ts b/apps/36-blocks-widget/src/app/app.component.ts
new file mode 100644
index 00000000..f274bdc5
--- /dev/null
+++ b/apps/36-blocks-widget/src/app/app.component.ts
@@ -0,0 +1,93 @@
+import { ChangeDetectionStrategy, Component, effect, inject, OnDestroy, OnInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { environment } from '../environments/environment';
+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 = '4512365c177425472369c0fa8351a15';
+const THEME: WidgetTheme = WidgetTheme.System;
+const TYPE: PublicScriptType = PublicScriptType.Authorization;
+const AUTH_TOKEN =
+ 'ZHN5YlVZcjRDR3U5NjNGSk5rVGFRejI0MEdCZWg3RWpUK0xVYzlvajJFMlM4a2F4NUpxaWJjcnJkVzZSeW5RMStZaWJaV1JYandHOXpsTlBVZXBnNUZWbzl5MFJHU0xyNEtMWUkxVjRSS0RiRXBOcER4czlxakJUdThLNUhnOFJGOHV3bXBHclAwN241d3dDM0JmZE9JMlhzNHZHaVZ6cGdvTkdIenJzbnNiMHFGNmhVMUpQbkZPUWRWOVFGcTRPQ2NBU1plM0lKUUFPTmI0cUVSUEJTdz09';
+
+@Component({
+ selector: 'proxy-root',
+ imports: [CommonModule],
+ templateUrl: './app.component.html',
+ styleUrls: ['./app.component.scss'],
+ host: { '(window:beforeunload)': 'ngOnDestroy()' },
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class AppComponent extends BaseComponent implements OnInit, OnDestroy {
+ private readonly themeService = inject(WidgetThemeService);
+
+ protected readonly showAuthentication: boolean = true;
+ protected readonly referenceId: string = REFERENCE_ID;
+ protected readonly theme: WidgetTheme = THEME;
+ protected readonly authToken: string = AUTH_TOKEN;
+ get containerId(): string {
+ return this.showAuthentication ? REFERENCE_ID : PROXY_DOM_ID;
+ }
+
+ constructor() {
+ super();
+ effect(() => {
+ const resolved = this.themeService.resolvedTheme();
+ const themeClass = `${resolved ?? WidgetTheme.System}-theme`;
+ document.body.classList.remove('light-theme', 'dark-theme', 'system-theme');
+ document.body.classList.add(themeClass);
+ });
+ }
+
+ ngOnInit(): void {
+ this.initOtpProvider();
+ }
+
+ public initOtpProvider(): void {
+ if (customElements.get('h-captcha') || (window as any).__proxyWidgetInitialized) {
+ return;
+ }
+ (window as any).__proxyWidgetInitialized = true;
+
+ if (!environment.production) {
+ const widgetConfig: WidgetConfig = {
+ referenceId: REFERENCE_ID, // Always pass referenceId
+ // showCompanyDetails: false,
+ // isHidden: true,
+ // loginRedirectUrl: 'https://www.google.com',
+ target: '_self',
+ success: (data) => {
+ console.log('success response', data);
+ },
+ failure: (error) => {
+ console.log('failure reason', error);
+ },
+ };
+ if (!this.showAuthentication) {
+ if (TYPE) {
+ widgetConfig['type'] = TYPE;
+ if (TYPE === PublicScriptType.Authorization) {
+ widgetConfig['isPreview'] = true;
+ } else {
+ widgetConfig['authToken'] = AUTH_TOKEN;
+
+ if (TYPE === PublicScriptType.UserManagement) {
+ // True If you want show Role and Permission tab
+ widgetConfig['isRolePermission'] = true;
+ }
+ }
+ }
+ }
+ if (THEME) {
+ widgetConfig['theme'] = THEME;
+ }
+ console.log('widgetConfig', widgetConfig);
+ window.initVerification(widgetConfig);
+ }
+ }
+
+ ngOnDestroy(): void {
+ super.ngOnDestroy();
+ }
+}
diff --git a/apps/36-blocks-widget/src/app/init-verification.ts b/apps/36-blocks-widget/src/app/init-verification.ts
new file mode 100644
index 00000000..e3df0f69
--- /dev/null
+++ b/apps/36-blocks-widget/src/app/init-verification.ts
@@ -0,0 +1,186 @@
+import { NgElement, WithProperties } from '@angular/elements';
+import { ProxyAuthWidgetComponent } from './otp/widget/widget.component';
+import { omit } from 'lodash-es';
+import { PROXY_DOM_ID, PublicScriptType } from '@proxy/constant';
+
+export const RESERVED_KEYS = ['referenceId', 'target', 'style', 'success', 'failure'];
+
+declare global {
+ interface Window {
+ initVerification: any;
+ intlTelInput: any;
+ showUserManagement: any;
+ hideUserManagement: any;
+ __proxyAuth: any;
+ __proxyAuthLoaded: boolean;
+ }
+}
+
+// Version metadata — helps clients debug CDN/cache issues
+window.__proxyAuth = window.__proxyAuth || {};
+window.__proxyAuth.version = '0.0.3';
+window.__proxyAuth.buildTime = new Date().toISOString();
+
+// Global function to show user management component (sets isHidden to false)
+if (!window.showUserManagement) {
+ window['showUserManagement'] = () => {
+ window.dispatchEvent(new CustomEvent('showUserManagement'));
+ };
+}
+
+// Global function to hide user management component (sets isHidden to true)
+if (!window.hideUserManagement) {
+ window['hideUserManagement'] = () => {
+ window.dispatchEvent(new CustomEvent('hideUserManagement'));
+ };
+}
+
+function documentReady(fn: any) {
+ // see if DOM is already available
+ if (document.readyState === 'complete' || document.readyState === 'interactive') {
+ // call on next available tick
+ setTimeout(fn, 1);
+ } else {
+ document.addEventListener('DOMContentLoaded', fn);
+ }
+}
+
+if (!window.initVerification) {
+ window['initVerification'] = (config: any) => {
+ documentReady(() => {
+ const urlParams = new URLSearchParams(window.location.search);
+ const isRegisterFormOnlyFromParams = urlParams.get('isRegisterFormOnly') === 'true';
+ const paramsData = {
+ ...(urlParams.get('first_name') && { firstName: urlParams.get('first_name') }),
+ ...(urlParams.get('last_name') && { lastName: urlParams.get('last_name') }),
+ ...(urlParams.get('email') && { email: urlParams.get('email') }),
+ ...(urlParams.get('signup_service_id') && { signupServiceId: urlParams.get('signup_service_id') }),
+ };
+ if (config?.referenceId || config?.authToken || config?.showCompanyDetails) {
+ const findOtpProvider = document.querySelector('proxy-auth');
+ if (findOtpProvider) {
+ const parentContainer = findOtpProvider.parentElement;
+ if (parentContainer) {
+ parentContainer.querySelectorAll('#skeleton-loader').forEach((el) => el.remove());
+ }
+ findOtpProvider.remove();
+ }
+ const widgetElement = document.createElement('proxy-auth') as NgElement &
+ WithProperties
;
+ widgetElement.referenceId = config?.referenceId;
+ widgetElement.type = config?.type;
+ widgetElement.authToken = config?.authToken;
+ widgetElement.showCompanyDetails = config?.showCompanyDetails;
+ widgetElement.userToken = config?.userToken;
+ widgetElement.isRolePermission = config?.isRolePermission;
+ widgetElement.isPreview = config?.isPreview;
+ widgetElement.isLogin = config?.isLogin;
+ widgetElement.loginRedirectUrl = config?.loginRedirectUrl;
+ widgetElement.theme = config?.theme;
+ widgetElement.version = config?.version;
+ widgetElement.input_fields = config?.input_fields;
+ widgetElement.show_social_login_icons = config?.show_social_login_icons;
+ widgetElement.exclude_role_ids = config?.exclude_role_ids;
+ widgetElement.include_role_ids = config?.include_role_ids;
+ widgetElement.isHidden = config?.isHidden;
+ widgetElement.isRegisterFormOnly = config?.isRegisterFormOnly || isRegisterFormOnlyFromParams;
+ widgetElement.target = config?.target ?? '_self';
+ if (!config.success || typeof config.success !== 'function') {
+ throw Error('success callback function missing !');
+ }
+ widgetElement.successReturn = config.success;
+ widgetElement.failureReturn = config.failure;
+
+ // omitting keys which are not required in API payload; query params fill in missing values
+ widgetElement.otherData = { ...paramsData, ...omit(config, RESERVED_KEYS) };
+
+ // Determine the target container id:
+ const FALLBACK_CONTAINER_ID = PROXY_DOM_ID;
+ const targetId: string =
+ config?.authToken || config?.type === PublicScriptType.Authorization
+ ? FALLBACK_CONTAINER_ID
+ : config?.referenceId;
+
+ const resolveContainer = (): HTMLElement | null => document.getElementById(targetId);
+
+ // Mount helper — called exactly once when the container is found.
+ const mountWidget = (container: HTMLElement): void => {
+ container.append(widgetElement);
+ window['libLoaded'] = true;
+ };
+
+ const container = resolveContainer();
+ if (container) {
+ mountWidget(container);
+ } else {
+ // SPA-safe retry strategy:
+ // 1. MutationObserver watches the entire document for the target element
+ // being added at any depth (handles Angular/React/Next.js async renders).
+ // 2. A setInterval heartbeat runs in parallel as a safety net for cases
+ // where MutationObserver misses the insertion (e.g. innerHTML swap).
+ // 3. Both are cancelled the moment the container is found.
+ // 4. After TIMEOUT_MS with no container, log a clear error and stop.
+
+ const RETRY_INTERVAL_MS = 150;
+ const TIMEOUT_MS = 30_000; // 30 s — covers slow/lazy SPA routes
+ let resolved = false;
+ const startTime = Date.now();
+
+ const cleanup = (observer: MutationObserver, intervalId: ReturnType): void => {
+ observer.disconnect();
+ clearInterval(intervalId);
+ };
+
+ const tryMount = (
+ observer: MutationObserver,
+ intervalId: ReturnType
+ ): boolean => {
+ if (resolved) return true;
+ const found = resolveContainer();
+ if (found) {
+ resolved = true;
+ cleanup(observer, intervalId);
+ mountWidget(found);
+ return true;
+ }
+ if (Date.now() - startTime >= TIMEOUT_MS) {
+ resolved = true;
+ cleanup(observer, intervalId);
+ console.error(
+ `[proxy-auth] Container element with id="${targetId}" was not found in the document ` +
+ `after ${TIMEOUT_MS / 1000} s. ` +
+ `Ensure the element exists in the DOM before calling window.initVerification().`
+ );
+ return true;
+ }
+ return false;
+ };
+
+ // Placeholder interval id — replaced after both are created.
+ let intervalId: ReturnType;
+
+ const observer = new MutationObserver(() => {
+ tryMount(observer, intervalId);
+ });
+
+ observer.observe(document.documentElement, {
+ childList: true,
+ subtree: true,
+ attributes: true,
+ attributeFilter: ['id'],
+ });
+
+ intervalId = setInterval(() => {
+ tryMount(observer, intervalId);
+ }, RETRY_INTERVAL_MS);
+ }
+ } else {
+ if (!config?.referenceId) {
+ throw Error('Reference Id is missing!');
+ } else {
+ throw Error('Something went wrong!');
+ }
+ }
+ });
+ };
+}
diff --git a/apps/proxy-auth/src/app/otp/component/index.ts b/apps/36-blocks-widget/src/app/otp/component/index.ts
similarity index 100%
rename from apps/proxy-auth/src/app/otp/component/index.ts
rename to apps/36-blocks-widget/src/app/otp/component/index.ts
diff --git a/apps/36-blocks-widget/src/app/otp/component/login/login.component.html b/apps/36-blocks-widget/src/app/otp/component/login/login.component.html
new file mode 100644
index 00000000..cd3d141a
--- /dev/null
+++ b/apps/36-blocks-widget/src/app/otp/component/login/login.component.html
@@ -0,0 +1,372 @@
+
+
+ @if (step > 1) {
+
+
+
+
+
+ } @else {
+
+ }
+
+
+
+
+
+
+@if (step === 1) {
+
+
Login
+
+
+ @if (loginForm.get('username'); as userNameControl) {
+
+
Email or Mobile
+
+ @if (userNameControl.touched && userNameControl.errors?.['required']) {
+
Email or Mobile number is required.
+ }
+
+ Note: Please enter your Mobile number with the country code (e.g. 91)
+
+
+ }
+
+
+ @if (loginForm.get('password'); as passwordControl) {
+
+
Password
+
+
+
+
+
+
+ @if (passwordControl.touched) { @if (passwordControl.errors?.['required']) {
+
Password is required.
+ } @else if (passwordControl.errors?.['pattern']) {
+
+ Password should contain atleast one Capital Letter, one Small Letter, one Digit and one Symbol.
+
+ } }
+
+ }
+
+
+
+ @if (isDark) {
+
+ } @if (!isDark) {
+
+ }
+
+
+ @if (apiError | async; as errorMsg) {
+
{{ errorMsg }}
+ }
+
+
+
+
+ @if (isLoading$ | async) {
+
+
+
+
+ } Login
+
+
+ New User?
+ Create Account
+
+
+
+} @if (step === 2) {
+
+
Reset Password
+
+
+
+ @if (apiError | async; as errorMsg) {
+
{{ errorMsg }}
+ }
+
+
+ @if (isLoading$ | async) {
+
+
+
+
+ } Send OTP
+
+
+} @if (step === 3) {
+
+
Change Password
+
+
+
{{ sendOtpForm.get('userDetails').value }}
+
Change
+
+
+
0"
+ class="self-start text-xs font-medium text-indigo-600 dark:text-indigo-400 hover:underline cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
+ >
+ Resend OTP @if (remainingSeconds > 0) { ({{ remainingSeconds }}s) }
+
+
+
+
+
+
+ @if (resetPasswordForm.get('password'); as passwordControl) {
+
+
Password
+
+
+
+
+
+
+ @if (passwordControl.touched) { @if (passwordControl.errors?.['required']) {
+
Password is required.
+ } @else if (passwordControl.errors?.['minlength']; as minLengthError) {
+
+ Min required length is {{ minLengthError?.requiredLength }}
+
+ } @else if (passwordControl.errors?.['pattern']) {
+
+ Password should contain atleast one Capital Letter, one Small Letter, one Digit and one Symbol.
+
+ } }
+
+ }
+
+
+
+
+ @if (apiError | async; as errorMsg) {
+
{{ errorMsg }}
+ }
+
+
+ @if (isLoading$ | async) {
+
+
+
+
+ } Submit
+
+
+}
+
+
+
+
+
{{ label }}
+
+ @if (formControl.touched) { @if (formControl.errors?.['required']) {
+
{{ label }} is required.
+ } @else if (formControl.errors?.['pattern']) {
+
+ {{ patternError ? patternError : 'Enter valid ' + label }}
+
+ } @else if (formControl.errors?.['minlength']; as minLengthError) {
+
+ Min required length is {{ minLengthError?.requiredLength }}
+
+ } @else if (formControl.errors?.['cannotContainSpace']) {
+
Whitespace not allowed
+ } @else if (formControl.errors?.['valueSameAsControl']) {
+
{{ label }} mismatch
+ } } @if (hint) {
+
{{ hint }}
+ }
+
+
+
+
+
+ @if (!showPassword) {
+
+
+
+ } @else {
+
+
+
+ }
+
diff --git a/apps/36-blocks-widget/src/app/otp/component/login/login.component.scss b/apps/36-blocks-widget/src/app/otp/component/login/login.component.scss
new file mode 100644
index 00000000..9175246c
--- /dev/null
+++ b/apps/36-blocks-widget/src/app/otp/component/login/login.component.scss
@@ -0,0 +1,5 @@
+:host {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+}
diff --git a/apps/proxy-auth/src/app/otp/component/login/login.component.ts b/apps/36-blocks-widget/src/app/otp/component/login/login.component.ts
similarity index 84%
rename from apps/proxy-auth/src/app/otp/component/login/login.component.ts
rename to apps/36-blocks-widget/src/app/otp/component/login/login.component.ts
index 3ad8a054..3b96b6c2 100644
--- a/apps/proxy-auth/src/app/otp/component/login/login.component.ts
+++ b/apps/36-blocks-widget/src/app/otp/component/login/login.component.ts
@@ -1,4 +1,17 @@
-import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ OnDestroy,
+ OnInit,
+ ViewChild,
+ effect,
+ inject,
+ input,
+ output,
+} from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { ReactiveFormsModule } from '@angular/forms';
+import { MarkAllAsTouchedDirective } from '@proxy/directives/mark-all-as-touched';
import { LoginComponentStore } from './login.store';
import { BehaviorSubject, filter, interval, Observable, Subscription, takeUntil } from 'rxjs';
import { IAppState } from '../../store/app.state';
@@ -10,24 +23,32 @@ import { FormControl, FormGroup, Validators } from '@angular/forms';
import { IlogInData, IOtpData, IResetPassword } from '../../model/otp';
import { EMAIL_OR_MOBILE_REGEX, PASSWORD_REGEX } from '@proxy/regex';
import { CustomValidators } from '@proxy/custom-validator';
-import { META_TAG_ID } from '@proxy/constant';
-import { environment } from 'apps/proxy-auth/src/environments/environment';
+import { META_TAG_ID, WidgetTheme } from '@proxy/constant';
+import { environment } from 'apps/36-blocks-widget/src/environments/environment';
import { OtpUtilityService } from '../../service/otp-utility.service';
+import { WidgetThemeService } from '../../service/widget-theme.service';
import { NgHcaptchaComponent } from 'ng-hcaptcha';
@Component({
selector: 'proxy-login',
+ imports: [CommonModule, ReactiveFormsModule, MarkAllAsTouchedDirective],
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss'],
providers: [LoginComponentStore],
+ changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LoginComponent extends BaseComponent implements OnInit, OnDestroy {
- @Input() public loginServiceData: any;
- @Input() public theme: string;
- @Output() public togglePopUp: EventEmitter = new EventEmitter();
- @Output() public closePopUp: EventEmitter = new EventEmitter();
- @Output() public openPopUp: EventEmitter = new EventEmitter();
- @Output() public failureReturn: EventEmitter = new EventEmitter();
+ public loginServiceData = input();
+ public theme = input();
+ protected readonly WidgetTheme = WidgetTheme;
+ private readonly themeService = inject(WidgetThemeService);
+ get isDark(): boolean {
+ return this.themeService.isDark(this.theme() as WidgetTheme);
+ }
+ public togglePopUp = output();
+ public closePopUp = output();
+ public openPopUp = output();
+ public failureReturn = output();
public state: string;
public step: number = 1;
public showPassword: boolean = false;
@@ -38,6 +59,10 @@ export class LoginComponent extends BaseComponent implements OnInit, OnDestroy {
public hCaptchaToken: string = '';
public hCaptchaVerified: boolean = false;
@ViewChild(NgHcaptchaComponent) hCaptchaComponent: NgHcaptchaComponent;
+ private componentStore = inject(LoginComponentStore);
+ private store = inject>(Store);
+ private otpUtilityService = inject(OtpUtilityService);
+
public otpData$: Observable = this.componentStore.otpdata$;
public isLoading$: Observable = this.componentStore.isLoading$;
public resetPassword$: Observable = this.componentStore.resetPassword$;
@@ -65,12 +90,9 @@ export class LoginComponent extends BaseComponent implements OnInit, OnDestroy {
]),
});
- constructor(
- private componentStore: LoginComponentStore,
- private store: Store,
- private otpUtilityService: OtpUtilityService
- ) {
+ constructor() {
super();
+ effect(() => this.themeService.setInputTheme(this.theme()));
this.selectWidgetData$ = this.store.pipe(select(selectWidgetData), takeUntil(this.destroy$));
}
diff --git a/apps/proxy-auth/src/app/otp/component/login/login.store.ts b/apps/36-blocks-widget/src/app/otp/component/login/login.store.ts
similarity index 94%
rename from apps/proxy-auth/src/app/otp/component/login/login.store.ts
rename to apps/36-blocks-widget/src/app/otp/component/login/login.store.ts
index c923df2e..653a6de5 100644
--- a/apps/proxy-auth/src/app/otp/component/login/login.store.ts
+++ b/apps/36-blocks-widget/src/app/otp/component/login/login.store.ts
@@ -1,5 +1,6 @@
-import { Injectable } from '@angular/core';
-import { ComponentStore, tapResponse } from '@ngrx/component-store';
+import { Injectable, inject } from '@angular/core';
+import { ComponentStore } from '@ngrx/component-store';
+import { tapResponse } from '@ngrx/operators';
import { Observable, switchMap } from 'rxjs';
import { OtpService } from '../../service/otp.service';
import { PrimeNgToastService } from '@proxy/ui/prime-ng-toast';
@@ -18,7 +19,10 @@ export interface ILoginInitialState {
@Injectable()
export class LoginComponentStore extends ComponentStore {
- constructor(private service: OtpService, private toast: PrimeNgToastService) {
+ private service = inject(OtpService);
+ private toast = inject(PrimeNgToastService);
+
+ constructor() {
super({
isLoading: false,
logInData: null,
diff --git a/apps/36-blocks-widget/src/app/otp/component/register/register.component.html b/apps/36-blocks-widget/src/app/otp/component/register/register.component.html
new file mode 100644
index 00000000..e02981c1
--- /dev/null
+++ b/apps/36-blocks-widget/src/app/otp/component/register/register.component.html
@@ -0,0 +1,413 @@
+
+@if (!isInDialog()) {
+
+
Register
+ @if (!isRegisterFormOnly()) {
+
+
+
+
+
+ }
+
+}
+
+
+
+
+
User Details
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @if (!isOtpVerified) {
+
+ @if (!isOtpSent) {
+ {{ (selectGetOtpInProcess$ | async) ? 'Sending...' : 'Get OTP' }}
+ } @else if (isOtpSent && !canResendOtp) { Resend in {{ resendTimer }}s } @else if (isOtpSent && canResendOtp
+ && !(selectGetOtpInProcess$ | async)) { Resend OTP } @else { Sending... }
+
+ } @else {
+
+ Verified
+
+
+
+
+ }
+
+
+
+ @if (isNumberChanged || isOtpVerified) {
+
+ }
+
+
+ @if (isOtpSent && !isOtpVerified) {
+
+
+ @if (otpError) {
+
{{ otpError }}
+ }
+
+ }
+
+
+
+
+
+
+
+
+
+ Note: Password should contain atleast one Capital Letter, one Small Letter,
+ one Digit and one Symbol
+
+
+
+ @if (showCompanyDetail) {
+
+
+ Company Details (Optional)
+
+
+
+
+ }
+
+
+
+
+ @if (apiError | async; as apiErrorList) {
+ Error:
+ @for (error of apiErrorList; track error) { {{ error }} } }
+
+
+ Submit
+
+
+
+
+
+
+
+
{{ label }}
+
+ @if (type === 'password' || type === 'confirm password') {
+
+
+
+
+ } @else {
+
+ }
+
+ @if (formControl?.touched) { @if (formControl.errors?.['required']) {
+
{{ label }} is required.
+ } @else if (formControl.errors?.['minlengthWithSpace']) {
+
Min required length is 3.
+ } @else if (formControl.errors?.['noStartEndSpaces']) {
+
Start and End spaces are not allowed.
+ } @else if (formControl.errors?.['min']; as minError) {
+
+ Min value required is {{ minError?.min }}.
+
+ } @else if (formControl.errors?.['max']; as maxError) {
+
+ Max value allowed is {{ maxError?.max }}.
+
+ } @else if (formControl.errors?.['minlength']; as minLengthError) {
+
+ Min required length is {{ minLengthError?.requiredLength }}.
+
+ } @else if (formControl.errors?.['maxlength']; as maxLengthError) {
+
+ Max allowed length is {{ maxLengthError?.requiredLength }}.
+
+ } @else if (formControl.errors?.['pattern']) {
+
+ {{ patternError ? patternError : 'Enter valid ' + label }}
+
+ } @else if (formControl.errors?.['valueSameAsControl']) {
+
{{ label }} mismatch.
+ } } @if (hint) {
+
{{ hint }}
+ }
+
+
+
+
+
+
+
+ @if (formControl.touched && !intlClass?.[key]?.[required ? 'isRequiredValidNumber' : 'isValidNumber']) {
+
Please enter valid mobile number.
+ } @if (formControl.errors?.['otpVerificationFailed']) {
+
Please verify the mobile number.
+ }
+
+
+
+
+
+ @if (!visible) {
+
+
+
+ } @else {
+
+
+
+ }
+
diff --git a/apps/36-blocks-widget/src/app/otp/component/register/register.component.scss b/apps/36-blocks-widget/src/app/otp/component/register/register.component.scss
new file mode 100644
index 00000000..b5f52a47
--- /dev/null
+++ b/apps/36-blocks-widget/src/app/otp/component/register/register.component.scss
@@ -0,0 +1,63 @@
+:host {
+ width: 100%;
+ min-height: 100px;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ text-align: start;
+}
+
+/* API-driven button hover color override */
+.has-hover-color:hover {
+ background-color: var(--btn-hover-color) !important;
+}
+
+/* intl-tel-input: force wrapper to fill container */
+:host ::ng-deep .iti {
+ width: 100%;
+}
+
+/* intl-tel-input: prevent flag from overlapping input text */
+:host ::ng-deep .iti input[type='tel'] {
+ padding-left: 52px !important;
+}
+
+/* intl-tel-input: invalid ring state */
+.invalid-input {
+ outline: 2px solid var(--proxy-error-40);
+ outline-offset: -1px;
+}
+
+/* Dark mode — .dark class is on the dialog portal ancestor */
+:host-context(.dark) ::ng-deep .iti .iti__country-list {
+ background-color: #1f2937;
+ border-color: #374151;
+ color: #f9fafb;
+}
+
+:host-context(.dark) ::ng-deep .iti__country {
+ color: #f9fafb !important;
+}
+
+:host-context(.dark) ::ng-deep .iti__country:hover,
+:host-context(.dark) ::ng-deep .iti__country.iti__highlight {
+ background-color: #312e81 !important;
+}
+
+:host-context(.dark) ::ng-deep .iti__dial-code {
+ color: #9ca3af;
+}
+
+:host-context(.dark) ::ng-deep .iti__divider {
+ border-bottom-color: #374151;
+}
+
+:host-context(.dark) ::ng-deep .iti__selected-flag:hover,
+:host-context(.dark) ::ng-deep .iti--allow-dropdown .iti__flag-container:hover .iti__selected-flag {
+ background-color: rgba(255, 255, 255, 0.08);
+}
+
+:host-context(.dark) ::ng-deep input[type='tel'] {
+ color: #f9fafb;
+ background-color: transparent;
+}
diff --git a/apps/proxy-auth/src/app/otp/component/register/register.component.ts b/apps/36-blocks-widget/src/app/otp/component/register/register.component.ts
similarity index 80%
rename from apps/proxy-auth/src/app/otp/component/register/register.component.ts
rename to apps/36-blocks-widget/src/app/otp/component/register/register.component.ts
index 9ebb1824..4f3a007a 100644
--- a/apps/proxy-auth/src/app/otp/component/register/register.component.ts
+++ b/apps/36-blocks-widget/src/app/otp/component/register/register.component.ts
@@ -1,18 +1,23 @@
import { cloneDeep } from 'lodash-es';
+import { WidgetTheme } from '@proxy/constant';
+import { CommonModule } from '@angular/common';
+import { ReactiveFormsModule } from '@angular/forms';
+import { MarkAllAsTouchedDirective } from '@proxy/directives/mark-all-as-touched';
import { OtpService } from './../../service/otp.service';
import { environment } from './../../../../environments/environment';
import {
AfterViewInit,
+ ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
- EventEmitter,
- Input,
OnDestroy,
OnInit,
- Output,
- SimpleChanges,
ViewChild,
ElementRef,
+ effect,
+ inject,
+ input,
+ output,
} from '@angular/core';
import { resetAll, resetAnyState, sendOtpAction, verifyOtpAction } from '../../store/actions/otp.action';
import { BaseComponent } from '@proxy/ui/base-component';
@@ -20,10 +25,11 @@ import { select, Store } from '@ngrx/store';
import { IAppState } from '../../store/app.state';
import { IntlPhoneLib, removeEmptyKeys } from '@proxy/utils';
import { FormControl, FormGroup, Validators } from '@angular/forms';
-import * as _ from 'lodash';
+import { isEqual } from 'lodash-es';
import { EMAIL_REGEX, NAME_REGEX, PASSWORD_REGEX } from '@proxy/regex';
import { CustomValidators } from '@proxy/custom-validator';
import { OtpUtilityService } from '../../service/otp-utility.service';
+import { WidgetThemeService } from '../../service/widget-theme.service';
import { errorResolver } from '@proxy/models/root-models';
import { BehaviorSubject, distinctUntilChanged, Observable, takeUntil, interval, Subscription } from 'rxjs';
import {
@@ -40,32 +46,35 @@ import { IGetOtpRes } from '../../model/otp';
@Component({
selector: 'proxy-register',
+ imports: [CommonModule, ReactiveFormsModule, MarkAllAsTouchedDirective],
templateUrl: './register.component.html',
styleUrls: ['./register.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
})
export class RegisterComponent extends BaseComponent implements AfterViewInit, OnDestroy, OnInit {
- @Input() public referenceId: string;
- @Input() public serviceData: any;
- @Input() public loginServiceData: any;
- @Input() public registrationViaLogin: boolean;
- @Input() public prefillDetails;
- @Input() public showCompanyDetails: boolean = true;
- @Input() public firstName: string;
- @Input() public lastName: string;
- @Input() public email: string;
- @Input() public signupServiceId: string | number;
- @Input() public isRegisterFormOnly: boolean = false;
- @Input() public version: string = 'v1';
- @Input() public theme: string;
+ public referenceId = input();
+ public serviceData = input();
+ public loginServiceData = input();
+ public registrationViaLogin = input();
+ public prefillDetails = input();
+ public showCompanyDetails = input(true);
+ public version = input('v1');
+ public theme = input();
+ protected readonly WidgetTheme = WidgetTheme;
+ public firstName = input();
+ public lastName = input();
+ public email = input();
+ public signupServiceId = input();
+ public isRegisterFormOnly = input(false);
+ public isInDialog = input(false);
public showPassword: boolean = false;
public showConfirmPassword: boolean = false;
- @Output() public togglePopUp: EventEmitter = new EventEmitter();
- @Output() public successReturn: EventEmitter = new EventEmitter();
- @Output() public failureReturn: EventEmitter = new EventEmitter();
+ public togglePopUp = output();
+ public successReturn = output();
+ public failureReturn = output();
get showCompanyDetail(): boolean {
- // Show company details by default, only hide when explicitly set to false
- return this.showCompanyDetails !== false;
+ return this.showCompanyDetails() !== false;
}
public registrationForm = new FormGroup({
@@ -138,57 +147,65 @@ export class RegisterComponent extends BaseComponent implements AfterViewInit, O
@ViewChild('otp3', { static: false }) otp3Ref: ElementRef;
@ViewChild('otp4', { static: false }) otp4Ref: ElementRef;
- constructor(
- private store: Store,
- private otpService: OtpService,
- private otpUtilityService: OtpUtilityService,
- private cdr: ChangeDetectorRef
- ) {
+ private store = inject>(Store);
+ private otpService = inject(OtpService);
+ private otpUtilityService = inject(OtpUtilityService);
+ private cdr = inject(ChangeDetectorRef);
+ private readonly themeService = inject(WidgetThemeService);
+ get isDarkTheme(): boolean {
+ return this.themeService.isDark(this.theme() as WidgetTheme);
+ }
+
+ constructor() {
super();
+ effect(() => this.themeService.setInputTheme(this.theme()));
this.selectGetOtpRes$ = this.store.pipe(
select(selectGetOtpRes),
- distinctUntilChanged(_.isEqual),
+ distinctUntilChanged(isEqual),
takeUntil(this.destroy$)
);
this.selectGetOtpInProcess$ = this.store.pipe(
select(selectGetOtpInProcess),
- distinctUntilChanged(_.isEqual),
+ distinctUntilChanged(isEqual),
takeUntil(this.destroy$)
);
this.selectGetOtpSuccess$ = this.store.pipe(
select(selectGetOtpSuccess),
- distinctUntilChanged(_.isEqual),
+ distinctUntilChanged(isEqual),
takeUntil(this.destroy$)
);
this.selectVerifyOtpV2Data$ = this.store.pipe(
select(selectVerifyOtpV2Data),
- distinctUntilChanged(_.isEqual),
+ distinctUntilChanged(isEqual),
takeUntil(this.destroy$)
);
this.selectVerifyOtpV2InProcess$ = this.store.pipe(
select(selectVerifyOtpV2InProcess),
- distinctUntilChanged(_.isEqual),
+ distinctUntilChanged(isEqual),
takeUntil(this.destroy$)
);
this.selectVerifyOtpV2Success$ = this.store.pipe(
select(selectVerifyOtpV2Success),
- distinctUntilChanged(_.isEqual),
+ distinctUntilChanged(isEqual),
takeUntil(this.destroy$)
);
this.selectApiErrorResponse$ = this.store.pipe(
select(selectApiErrorResponse),
- distinctUntilChanged(_.isEqual),
+ distinctUntilChanged(isEqual),
takeUntil(this.destroy$)
);
this.selectWidgetTheme$ = this.store.pipe(
select(selectWidgetTheme),
- distinctUntilChanged(_.isEqual),
+ distinctUntilChanged(isEqual),
takeUntil(this.destroy$)
);
}
ngOnInit(): void {
- if (this.isRegisterFormOnly) {
+ this.selectWidgetTheme$.pipe(takeUntil(this.destroy$)).subscribe((theme) => {
+ this.uiPreferences = theme?.ui_preferences || {};
+ });
+ if (this.isRegisterFormOnly()) {
this.registrationForm.get('user.email').disable();
}
this.selectWidgetTheme$.pipe(takeUntil(this.destroy$)).subscribe((theme) => {
@@ -216,16 +233,19 @@ export class RegisterComponent extends BaseComponent implements AfterViewInit, O
this.registrationForm.get('user.mobile').setErrors(null);
this.otpError = ''; // Clear error on successful verification
}
+ this.cdr.markForCheck();
});
this.selectVerifyOtpV2Data$.pipe(takeUntil(this.destroy$)).subscribe((res) => {
this.otpVerificationToken = res?.data?.otp_verification_token;
+ this.cdr.markForCheck();
});
this.selectGetOtpSuccess$.pipe(takeUntil(this.destroy$)).subscribe((res) => {
- this.isOtpSent = res;
if (res) {
+ this.isOtpSent = true;
this.startResendTimer();
this.lastSentMobileNumber = this.registrationForm.get('user.mobile').value;
this.isNumberChanged = true;
+ this.cdr.markForCheck();
}
});
@@ -236,48 +256,38 @@ export class RegisterComponent extends BaseComponent implements AfterViewInit, O
// Clear OTP form to allow user to retry
this.otpForm.reset();
}
+ this.cdr.markForCheck();
});
// Add global paste event listener
document.addEventListener('paste', this.handleGlobalPaste.bind(this));
}
- ngOnChanges(changes: SimpleChanges) {
- if (changes?.prefillDetails?.currentValue) {
- this.checkPrefillDetails();
- }
- if (changes?.firstName?.currentValue) {
- this.registrationForm.get('user.firstName').setValue(changes.firstName.currentValue);
- }
- if (changes?.lastName?.currentValue) {
- this.registrationForm.get('user.lastName').setValue(changes.lastName.currentValue);
- }
- if (changes?.email?.currentValue) {
- this.registrationForm.get('user.email').setValue(changes.email.currentValue);
- }
- }
checkPrefillDetails() {
- if (isNaN(Number(this.prefillDetails))) {
- this.registrationForm.get('user.email').setValue(this.prefillDetails);
+ const val = this.prefillDetails();
+ if (isNaN(Number(val))) {
+ this.registrationForm.get('user.email').setValue(val);
this.registrationForm.get('user.mobile').setValue(null);
} else {
this.registrationForm.get('user.email').setValue(null);
- this.prefilledNumber = this.prefillDetails;
- this.registrationForm.get('user.mobile').setValue(this.prefillDetails);
+ this.prefilledNumber = val;
+ this.registrationForm.get('user.mobile').setValue(val);
}
}
ngAfterViewInit(): void {
- this.initIntl('user');
- let count = 0;
- const userIntlWrapper = document
- ?.querySelector('proxy-auth')
- ?.shadowRoot?.querySelector('#init-contact-wrapper-user');
- const interval = setInterval(() => {
- if (count > 6 || userIntlWrapper?.querySelector('.iti__selected-flag')?.getAttribute('title')) {
- this.initIntl('company');
- clearInterval(interval);
- }
- count += 1;
- }, 500);
+ setTimeout(() => {
+ this.initIntl('user');
+ let count = 0;
+ const interval = setInterval(() => {
+ const userIntlWrapper =
+ document.querySelector('proxy-auth')?.shadowRoot?.querySelector('#init-contact-wrapper-user') ||
+ document.getElementById('init-contact-wrapper-user');
+ if (count > 6 || userIntlWrapper?.querySelector('.iti__selected-flag')?.getAttribute('title')) {
+ this.initIntl('company');
+ clearInterval(interval);
+ }
+ count += 1;
+ }, 500);
+ });
}
public ngOnDestroy(): void {
@@ -328,7 +338,7 @@ export class RegisterComponent extends BaseComponent implements AfterViewInit, O
this.store.dispatch(
sendOtpAction({
request: {
- referenceId: this.referenceId,
+ referenceId: this.referenceId(),
mobile: mobileControl.value,
authkey: environment.sendOtpAuthKey,
},
@@ -338,11 +348,11 @@ export class RegisterComponent extends BaseComponent implements AfterViewInit, O
}
public initIntl(key: string): void {
- const parentDom = document.querySelector('proxy-auth')?.shadowRoot;
- const input = document.querySelector('proxy-auth')?.shadowRoot?.getElementById('init-contact-' + key);
- const customCssStyleURL = `${environment.baseUrl}/assets/utils/intl-tel-input-custom.css`;
+ const input = (document.querySelector('proxy-auth')?.shadowRoot?.getElementById('init-contact-' + key) ||
+ document.getElementById('init-contact-' + key)) as HTMLElement;
+ const customCssStyleURL = `${window.location.origin}/assets/utils/intl-tel-input-custom.css`;
if (input) {
- this.intlClass[key] = new IntlPhoneLib(input, parentDom, customCssStyleURL);
+ this.intlClass[key] = new IntlPhoneLib(input, document.head, customCssStyleURL);
if (this.prefilledNumber) {
input.setAttribute('value', `+${this.prefilledNumber}`);
}
@@ -422,7 +432,7 @@ export class RegisterComponent extends BaseComponent implements AfterViewInit, O
const formData = removeEmptyKeys(cloneDeep(this.registrationForm.getRawValue()), true);
const state = JSON.parse(
this.otpUtilityService.aesDecrypt(
- this.registrationViaLogin ? this.loginServiceData.state : this.serviceData?.state ?? '',
+ this.registrationViaLogin() ? this.loginServiceData().state : this.serviceData()?.state ?? '',
environment.uiEncodeKey,
environment.uiIvKey,
true
@@ -440,11 +450,13 @@ export class RegisterComponent extends BaseComponent implements AfterViewInit, O
formData.company['meta'] = {};
}
const payload = {
- reference_id: this.referenceId,
- service_id: this.registrationViaLogin ? this.loginServiceData.service_id : this.serviceData.service_id,
+ reference_id: this.referenceId(),
+ service_id: this.registrationViaLogin()
+ ? this.loginServiceData().service_id
+ : this.serviceData().service_id,
url_unique_id: state?.url_unique_id,
request_data: formData,
- ...(this.signupServiceId && { signup_service_id: this.signupServiceId }),
+ ...(this.signupServiceId() && { signup_service_id: this.signupServiceId() }),
};
const encodedData = this.otpUtilityService.aesEncrypt(
JSON.stringify(payload),
@@ -452,7 +464,9 @@ export class RegisterComponent extends BaseComponent implements AfterViewInit, O
environment.uiIvKey,
true
);
- const registrationState = this.registrationViaLogin ? this.loginServiceData.state : this.serviceData.state;
+ const registrationState = this.registrationViaLogin()
+ ? this.loginServiceData().state
+ : this.serviceData().state;
this.otpService
.register({
proxy_state: encodedData,
@@ -484,7 +498,7 @@ export class RegisterComponent extends BaseComponent implements AfterViewInit, O
this.store.dispatch(
sendOtpAction({
request: {
- referenceId: this.referenceId,
+ referenceId: this.referenceId(),
mobile: mobileControl.value,
authkey: environment.sendOtpAuthKey,
},
@@ -503,7 +517,7 @@ export class RegisterComponent extends BaseComponent implements AfterViewInit, O
this.store.dispatch(
verifyOtpAction({
request: {
- referenceId: this.referenceId,
+ referenceId: this.referenceId(),
mobile: mobileControl.value,
otp: otpString,
authkey: environment.sendOtpAuthKey,
@@ -623,17 +637,17 @@ export class RegisterComponent extends BaseComponent implements AfterViewInit, O
}
public get primaryColor(): string | null {
- if (this.version !== 'v2') {
+ if (this.version() !== 'v2') {
return null;
}
- const isDark = this.theme === 'dark';
+ const isDark = this.themeService.isDark();
return isDark
? this.uiPreferences?.dark_theme_primary_color || null
: this.uiPreferences?.light_theme_primary_color || null;
}
public get borderRadiusValue(): string | null {
- if (this.version !== 'v2') {
+ if (this.version() !== 'v2') {
return null;
}
switch (this.uiPreferences?.border_radius) {
@@ -651,21 +665,17 @@ export class RegisterComponent extends BaseComponent implements AfterViewInit, O
}
public get buttonColor(): string | null {
- if (this.version !== 'v2') return null;
+ if (this.version() !== 'v2') return null;
return this.uiPreferences?.button_color || null;
}
public get buttonHoverColor(): string | null {
- if (this.version !== 'v2') return null;
+ if (this.version() !== 'v2') return null;
return this.uiPreferences?.button_hover_color || null;
}
public get buttonTextColor(): string | null {
- if (this.version !== 'v2') return null;
+ if (this.version() !== 'v2') return null;
return this.uiPreferences?.button_text_color || null;
}
-
- public get isDarkTheme(): boolean {
- return this.theme === 'dark';
- }
}
diff --git a/apps/36-blocks-widget/src/app/otp/component/send-otp-center/send-otp-center.component.html b/apps/36-blocks-widget/src/app/otp/component/send-otp-center/send-otp-center.component.html
new file mode 100644
index 00000000..36502c69
--- /dev/null
+++ b/apps/36-blocks-widget/src/app/otp/component/send-otp-center/send-otp-center.component.html
@@ -0,0 +1,629 @@
+
+@if (isUserProxyContainer()) {
+
+ Click Here to preview this Reference ID: {{ referenceId() }}
+
+
+
+
+
+ Preview
+
+
+}
+
+
+@if (!isUserProxyContainer()) {
+
+@if (!hideInlineHeader()) {
+
+
+
+
+
+}
+
+
+
+
+}
+
+
+
+ @if (selectWidgetData$ | async; as widgetDataArray) {
+
+
+ @for (widgetData of widgetDataArray; track widgetData.service_id) { @if (widgetData?.service_id ===
+ featureServiceIds.PasswordAuthentication && version() === 'v2' && loginStep === 1) { @if (selectWidgetTheme$ |
+ async; as widgetTheme) { @if (widgetTheme?.ui_preferences?.logo_url) {
+
+
+
+ } }
+
+ {{ titleText }}
+
+ } }
+
+
+ @if (input_fields() === 'top') {
+
+
+
+ }
+
+
+ @if (input_fields() === 'bottom') {
+
+
+
+ }
+
+
+ @if (loginStep === 1 && widgetDataArray?.length && isCreateAccountLink()) {
+
+ Are you a new User?
+ {{ signUpButtonText }}
+
+ } @if (!widgetDataArray?.length) {
+ No Service Enabled
+ } }
+
+
+
+@if (dialogOpen()) {
+
+
+
+
+
+
+
+
+
+
+ @if (showRegistrationInDialog() || loginStep === 2 || loginStep === 3) {
+
+
+
+
+
+ } @else {
+
+ }
+
+
+
+ @if (showRegistrationInDialog()) { Create Account } @else if (loginStep === 2 || loginStep === 3) {
+ Forgot Password } @else { Sign In }
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @if (!showRegistrationInDialog()) {
+
+
+
+ }
+
+
+ @if (showRegistrationInDialog()) {
+
+ }
+
+
+
+}
+
+
+
+ @if (!showPassword) {
+
+
+
+ } @else {
+
+
+
+ }
+
+
+
+
+ @for (widgetData of widgetDataArray; track widgetData.service_id) { @if (widgetData?.service_id ===
+ featureServiceIds.PasswordAuthentication && version() === 'v2' && loginStep === 1) { @if
+ (hasOtherAuthOptions(widgetDataArray)) {
+
+ } } }
+
+
+
+
+ @for (widgetData of widgetDataArray; track widgetData.service_id) { @if (widgetData?.service_id ===
+ featureServiceIds.PasswordAuthentication && version() === 'v2') {
+
+
+ @if (loginStep === 1) {
+
+
+ @if (loginForm.get('username'); as userNameControl) {
+
+
Email or Mobile
+
+
+ Note: Enter Mobile number with country code (e.g. 91)
+
+ @if (userNameControl.touched && userNameControl.errors?.['required']) {
+
+ Email or Mobile number is required.
+
+ }
+
+ }
+
+
+ @if (loginForm.get('password'); as passwordControl) {
+
+
Password
+
+
+
+
+
+
+ @if (passwordControl.touched && passwordControl.errors?.['required']) {
+
+ Password is required.
+
+ } @if (passwordControl.touched && passwordControl.errors?.['cannotContainSpace']) {
+
Whitespace not allowed.
+ }
+
+ }
+
+
+
+ @if (isDarkTheme) {
+
+ } @if (!isDarkTheme) {
+
+ }
+
+
+ @if (loginError) {
+
{{ loginError }}
+ }
+
+
+
+
+ Forgot Password?
+
+
+ @if (isLoginLoading$ | async) {
+
+
+
+
+ } Sign in
+
+
+
+ }
+
+
+ @if (loginStep === 2) {
+
+ @if (!hideInlineHeader()) {
+
+ Reset Password
+
+ }
+
+
+
Email or Mobile
+
+ @if (sendOtpLoginForm.get('userDetails')?.touched &&
+ sendOtpLoginForm.get('userDetails')?.errors?.['required']) {
+
+ Email or Mobile is required.
+
+ } @if (sendOtpLoginForm.get('userDetails')?.touched &&
+ sendOtpLoginForm.get('userDetails')?.errors?.['pattern']) {
+
Enter a valid Email or Mobile.
+ }
+
+
+ @if (loginError) {
+
{{ loginError }}
+ }
+
+
+ @if (isLoginLoading$ | async) {
+
+
+
+
+ } Send OTP
+
+
+ }
+
+
+ @if (loginStep === 3) {
+
+
+ Change Password
+
+
+
+ {{ sendOtpLoginForm.get('userDetails')?.value }}
+ Change
+
+
+
0"
+ class="w-link self-start"
+ >
+ Resend OTP @if (remainingSeconds > 0) {
+ ({{ remainingSeconds }}s)
+ }
+
+
+
+
+
OTP
+
+ @if (resetPasswordForm.get('otp')?.touched && resetPasswordForm.get('otp')?.errors?.['required']) {
+
+ OTP is required.
+
+ }
+
+
+
+ @if (resetPasswordForm.get('password'); as passwordControl) {
+
+
Password
+
+
+
+
+
+
+ @if (passwordControl.touched && passwordControl.errors?.['required']) {
+
+ Password is required.
+
+ } @if (passwordControl.touched && passwordControl.errors?.['minlength']; as minLengthError) {
+
+ Min required length is {{ minLengthError?.['requiredLength'] }}.
+
+ } @if (passwordControl.touched && passwordControl.errors?.['pattern']) {
+
+ Must contain uppercase, lowercase, digit and symbol.
+
+ }
+
+ }
+
+
+ @if (resetPasswordForm.get('confirmPassword'); as confirmPasswordControl) {
+
+
Confirm Password
+
+ @if (confirmPasswordControl.touched && confirmPasswordControl.errors?.['required']) {
+
+ Confirm Password is required.
+
+ } @if (confirmPasswordControl.touched && confirmPasswordControl.errors?.['minlength']; as minLengthError) {
+
+ Min required length is {{ minLengthError?.['requiredLength'] }}.
+
+ } @if (confirmPasswordControl.touched && confirmPasswordControl.errors?.['pattern']) {
+
+ Must contain uppercase, lowercase, digit and symbol.
+
+ } @if (confirmPasswordControl.touched && confirmPasswordControl.errors?.['valueSameAsControl']) {
+
Passwords do not match.
+ }
+
+ } @if (loginError) {
+
{{ loginError }}
+ }
+
+
+ @if (isLoginLoading$ | async) {
+
+
+
+
+ } Submit
+
+
+ } } }
+
+
+
+
+ @if (loginStep === 1) {
+
+
+ @if (show_social_login_icons()) {
+
+ @for (widgetData of widgetDataArray; track widgetData.service_id) { @if (widgetData?.service_id !==
+ featureServiceIds.PasswordAuthentication || (widgetData?.service_id === featureServiceIds.PasswordAuthentication
+ && version() === 'v1')) { @if (widgetData?.service_id !== featureServiceIds.Msg91OtpService ||
+ !(otpScriptLoading | async)) {
+
+
+
+ } } }
+
+ }
+
+
+ @if (!show_social_login_icons()) {
+
+ @for (widgetData of widgetDataArray; track widgetData.service_id) { @if (widgetData?.service_id !==
+ featureServiceIds.PasswordAuthentication || (widgetData?.service_id === featureServiceIds.PasswordAuthentication
+ && version() === 'v1')) { @if (widgetData?.service_id !== featureServiceIds.Msg91OtpService ||
+ !(otpScriptLoading | async)) {
+
+
+ {{ widgetData.text }}
+
+ } } }
+
+ } }
+
diff --git a/apps/36-blocks-widget/src/app/otp/component/send-otp-center/send-otp-center.component.scss b/apps/36-blocks-widget/src/app/otp/component/send-otp-center/send-otp-center.component.scss
new file mode 100644
index 00000000..7ce9555d
--- /dev/null
+++ b/apps/36-blocks-widget/src/app/otp/component/send-otp-center/send-otp-center.component.scss
@@ -0,0 +1,21 @@
+// :host {
+// display: flex;
+// flex-direction: column;
+// justify-content: center;
+// position: relative;
+// }
+
+// /* API-driven button hover color override */
+// .has-hover-color:hover {
+// background-color: var(--btn-hover-color) !important;
+// }
+
+// /* intl-tel-input: force wrapper to fill container */
+// :host ::ng-deep .iti {
+// width: 100%;
+// }
+
+// /* intl-tel-input: prevent flag from overlapping input text */
+// :host ::ng-deep .iti input[type='tel'] {
+// padding-left: 52px !important;
+// }
diff --git a/apps/proxy-auth/src/app/otp/component/send-otp-center/send-otp-center.component.ts b/apps/36-blocks-widget/src/app/otp/component/send-otp-center/send-otp-center.component.ts
similarity index 82%
rename from apps/proxy-auth/src/app/otp/component/send-otp-center/send-otp-center.component.ts
rename to apps/36-blocks-widget/src/app/otp/component/send-otp-center/send-otp-center.component.ts
index a255acbd..d8574c7d 100644
--- a/apps/proxy-auth/src/app/otp/component/send-otp-center/send-otp-center.component.ts
+++ b/apps/36-blocks-widget/src/app/otp/component/send-otp-center/send-otp-center.component.ts
@@ -1,18 +1,25 @@
import { OtpWidgetService } from './../../service/otp-widget.service';
+import { CommonModule } from '@angular/common';
+import { ReactiveFormsModule } from '@angular/forms';
+import { NgHcaptchaModule } from 'ng-hcaptcha';
import {
AfterViewInit,
+ ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
- EventEmitter,
- Input,
OnDestroy,
OnInit,
- Output,
ViewChild,
+ effect,
+ inject,
+ input,
+ output,
+ signal,
} from '@angular/core';
+import { WidgetPortalRef, WidgetPortalService } from '../../service/widget-portal.service';
import { NgHcaptchaComponent } from 'ng-hcaptcha';
-import { FormControl, FormGroup, UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms';
+import { FormControl, FormGroup, Validators } from '@angular/forms';
import { select, Store } from '@ngrx/store';
import { CustomValidators } from '@proxy/custom-validator';
import { BaseComponent } from '@proxy/ui/base-component';
@@ -46,60 +53,57 @@ import { debounceTime, distinctUntilChanged, skip, take, takeUntil } from 'rxjs/
import { EMAIL_REGEX, EMAIL_OR_MOBILE_REGEX, ONLY_INTEGER_REGEX, PASSWORD_REGEX } from '@proxy/regex';
import { IGetOtpRes, IlogInData, IOtpData, IResetPassword, IWidgetResponse } from '../../model/otp';
import { IntlPhoneLib } from '@proxy/utils';
-import { META_TAG_ID } from '@proxy/constant';
-import { environment } from 'apps/proxy-auth/src/environments/environment';
+import { META_TAG_ID, WidgetTheme } from '@proxy/constant';
+import { environment } from 'apps/36-blocks-widget/src/environments/environment';
import { FeatureServiceIds } from '@proxy/models/features-model';
import { LoginComponentStore } from '../login/login.store';
import { OtpUtilityService } from '../../service/otp-utility.service';
-
-export enum OtpErrorCodes {
- VerifyLimitReached = 704,
- InvalidOtp = 703,
-}
-
-export enum SendOtpCenterVersion {
- V1 = 'v1',
- V2 = 'v2',
-}
-
-export enum InputFields {
- TOP = 'top',
- BOTTOM = 'bottom',
-}
+import { WidgetThemeService } from '../../service/widget-theme.service';
+import { InputFields, OtpErrorCodes, WidgetVersion } from './utility/model';
+import { RegisterComponent } from '../register/register.component';
@Component({
- selector: 'proxy-send-otp-center',
+ selector: 'authorization',
+ imports: [CommonModule, ReactiveFormsModule, NgHcaptchaModule, RegisterComponent],
templateUrl: './send-otp-center.component.html',
styleUrls: ['./send-otp-center.component.scss'],
providers: [LoginComponentStore],
+ changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SendOtpCenterComponent extends BaseComponent implements OnInit, OnDestroy, AfterViewInit {
@ViewChild('initContact') initContact: ElementRef;
@ViewChild(NgHcaptchaComponent) hCaptchaComponent: NgHcaptchaComponent;
- @Input() public referenceId: string;
- @Input() public serviceData: any;
- @Input() public tokenAuth: string;
- @Input() public target: string;
- @Input() public version: SendOtpCenterVersion = SendOtpCenterVersion.V1;
- @Input() public input_fields: string = InputFields.TOP;
- @Input() public show_social_login_icons: boolean = false;
- @Input() public isCreateAccountLink: boolean;
- @Input() public theme: string;
- @Input() public isUserProxyContainer: boolean = true;
- @Output() public togglePopUp: EventEmitter = new EventEmitter();
- @Output() public successReturn: EventEmitter = new EventEmitter();
- @Output() public failureReturn: EventEmitter = new EventEmitter();
- @Output() public openPopUp: EventEmitter = new EventEmitter();
- @Output() public closePopUp: EventEmitter = new EventEmitter();
-
+ @ViewChild('dialogWrap') dialogWrapRef: ElementRef;
+ public referenceId = input();
+ public serviceData = input();
+ public tokenAuth = input();
+ public target = input();
+ public version = input(WidgetVersion.V1);
+ public input_fields = input(InputFields.TOP);
+ public show_social_login_icons = input(false);
+ public isCreateAccountLink = input();
+ public theme = input();
+ protected readonly WidgetTheme = WidgetTheme;
+ public isUserProxyContainer = input(true);
+ public hideInlineHeader = input(false);
+ public togglePopUp = output();
+ public successReturn = output();
+ public failureReturn = output();
+ public openPopUp = output();
+ public closePopUp = output();
+
+ private readonly themeService = inject(WidgetThemeService);
+ get isDarkTheme(): boolean {
+ return this.themeService.isDark(this.theme() as WidgetTheme);
+ }
public steps = 1;
- public phoneForm = new UntypedFormGroup({
- phone: new UntypedFormControl('', [Validators.required]),
+ public phoneForm = new FormGroup({
+ phone: new FormControl('', [Validators.required]),
});
- public otpControl = new UntypedFormControl(undefined, [
+ public otpControl = new FormControl(undefined, [
Validators.required,
Validators.pattern(ONLY_INTEGER_REGEX),
]);
- public emailControl = new UntypedFormControl('', [Validators.required, Validators.pattern(EMAIL_REGEX)]);
+ public emailControl = new FormControl('', [Validators.required, Validators.pattern(EMAIL_REGEX)]);
public errors$: Observable;
public selectGetOtpInProcess$: Observable;
@@ -117,6 +121,8 @@ export class SendOtpCenterComponent extends BaseComponent implements OnInit, OnD
public selectApiErrorResponse$: Observable;
public closeWidgetApiFailed$: Observable;
+ private otpWidgetService = inject(OtpWidgetService);
+
public otpScriptLoading: BehaviorSubject = this.otpWidgetService.scriptLoading;
public timerSubscription: Subscription;
@@ -181,15 +187,20 @@ export class SendOtpCenterComponent extends BaseComponent implements OnInit, OnD
public uiPreferences: any = {};
- constructor(
- private store: Store,
- private cdr: ChangeDetectorRef,
- private _elemRef: ElementRef,
- private otpWidgetService: OtpWidgetService,
- private loginComponentStore: LoginComponentStore,
- private otpUtilityService: OtpUtilityService
- ) {
+ public readonly dialogOpen = signal(false);
+ public readonly showRegistrationInDialog = signal(false);
+ private dialogPortalRef: WidgetPortalRef | null = null;
+
+ private store = inject>(Store);
+ private cdr = inject(ChangeDetectorRef);
+ private _elemRef = inject(ElementRef);
+ private loginComponentStore = inject(LoginComponentStore);
+ private otpUtilityService = inject(OtpUtilityService);
+ private readonly widgetPortal = inject(WidgetPortalService);
+
+ constructor() {
super();
+ effect(() => this.themeService.setInputTheme(this.theme()));
this.errors$ = this.store.pipe(select(errors), distinctUntilChanged(isEqual), takeUntil(this.destroy$));
this.selectGetOtpInProcess$ = this.store.pipe(
select(selectGetOtpInProcess),
@@ -403,7 +414,7 @@ export class SendOtpCenterComponent extends BaseComponent implements OnInit, OnD
sendOtpAction({
request: {
// variables: {},
- referenceId: this.referenceId,
+ referenceId: this.referenceId(),
mobile: this.mobileNumber,
},
})
@@ -427,8 +438,8 @@ export class SendOtpCenterComponent extends BaseComponent implements OnInit, OnD
this.store.dispatch(
getOtpResendAction({
request: {
- tokenAuth: this.tokenAuth,
- referenceId: this.referenceId,
+ tokenAuth: this.tokenAuth(),
+ referenceId: this.referenceId(),
reqId: this.otpRes.reqId,
retryChannel: channel,
},
@@ -441,9 +452,9 @@ export class SendOtpCenterComponent extends BaseComponent implements OnInit, OnD
this.store.dispatch(
getOtpVerifyAction({
request: {
- tokenAuth: this.tokenAuth,
+ tokenAuth: this.tokenAuth(),
otp: this.otpControl.value,
- referenceId: this.referenceId,
+ referenceId: this.referenceId(),
reqId: this.otpRes.reqId,
// identifier: this.sendOTPMode === '1' ? this.mobileNumber : this.emailControl.value,
},
@@ -459,7 +470,7 @@ export class SendOtpCenterComponent extends BaseComponent implements OnInit, OnD
public close(closeByUser: boolean = false) {
document.getElementById(META_TAG_ID)?.remove();
- if (this.isUserProxyContainer) {
+ if (this.isUserProxyContainer()) {
this.resetStore();
}
this.togglePopUp.emit();
@@ -514,7 +525,7 @@ export class SendOtpCenterComponent extends BaseComponent implements OnInit, OnD
}
private openLink(link: string): void {
- window.open(link, this.target);
+ window.open(link, this.target());
}
public onVerificationBtnClick(widgetData: any): void {
@@ -523,7 +534,7 @@ export class SendOtpCenterComponent extends BaseComponent implements OnInit, OnD
} else if (widgetData?.service_id === FeatureServiceIds.Msg91OtpService) {
this.otpWidgetService.openWidget();
} else if (widgetData?.service_id === FeatureServiceIds.PasswordAuthentication) {
- if (this.version === SendOtpCenterVersion.V2) {
+ if (this.version() === WidgetVersion.V2) {
this.login();
} else {
this.otpWidgetService.openLogin(true);
@@ -590,7 +601,33 @@ export class SendOtpCenterComponent extends BaseComponent implements OnInit, OnD
}
public showRegistration(prefillDetails?: string) {
- this.openPopUp.emit(prefillDetails || this.loginForm.get('username')?.value);
+ if (this.isUserProxyContainer()) {
+ this.showRegistrationInDialog.set(true);
+ } else {
+ this.openPopUp.emit(prefillDetails || this.loginForm.get('username')?.value);
+ }
+ }
+
+ public openPreviewDialog(): void {
+ this.dialogOpen.set(true);
+ this.showRegistrationInDialog.set(false);
+ setTimeout(() => {
+ if (this.dialogWrapRef?.nativeElement) {
+ this.dialogPortalRef = this.widgetPortal.attach(this.dialogWrapRef.nativeElement);
+ }
+ });
+ }
+
+ public closePreviewDialog(): void {
+ this.dialogPortalRef?.detach();
+ this.dialogPortalRef = null;
+ this.dialogOpen.set(false);
+ this.showRegistrationInDialog.set(false);
+ this.changeLoginStep(1);
+ }
+
+ public goBackFromRegistration(): void {
+ this.showRegistrationInDialog.set(false);
}
public hasOtherAuthOptions(widgetDataArray: any[]): boolean {
@@ -638,24 +675,24 @@ export class SendOtpCenterComponent extends BaseComponent implements OnInit, OnD
}
public get titleText(): string {
- if (this.version === SendOtpCenterVersion.V2 && this.uiPreferences?.title) {
+ if (this.version() === WidgetVersion.V2 && this.uiPreferences?.title) {
return this.uiPreferences.title;
}
return 'Login';
}
public get primaryColor(): string | null {
- if (this.version !== SendOtpCenterVersion.V2) {
+ if (this.version() !== WidgetVersion.V2) {
return null;
}
- const isDark = this.theme === 'dark';
+ const isDark = this.themeService.isDark();
return isDark
? this.uiPreferences?.dark_theme_primary_color || null
: this.uiPreferences?.light_theme_primary_color || null;
}
public get borderRadiusValue(): string | null {
- if (this.version !== SendOtpCenterVersion.V2) {
+ if (this.version() !== WidgetVersion.V2) {
return null;
}
switch (this.uiPreferences?.border_radius) {
@@ -673,24 +710,20 @@ export class SendOtpCenterComponent extends BaseComponent implements OnInit, OnD
}
public get buttonColor(): string | null {
- if (this.version !== SendOtpCenterVersion.V2) return null;
+ if (this.version() !== WidgetVersion.V2) return null;
return this.uiPreferences?.button_color || null;
}
public get buttonHoverColor(): string | null {
- if (this.version !== SendOtpCenterVersion.V2) return null;
+ if (this.version() !== WidgetVersion.V2) return null;
return this.uiPreferences?.button_hover_color || null;
}
public get buttonTextColor(): string | null {
- if (this.version !== SendOtpCenterVersion.V2) return null;
+ if (this.version() !== WidgetVersion.V2) return null;
return this.uiPreferences?.button_text_color || null;
}
- public get isDarkTheme(): boolean {
- return this.theme === 'dark';
- }
-
public get signUpButtonText(): string {
return this.uiPreferences?.sign_up_button_text || 'Create an account';
}
diff --git a/apps/36-blocks-widget/src/app/otp/component/send-otp-center/utility/model.ts b/apps/36-blocks-widget/src/app/otp/component/send-otp-center/utility/model.ts
new file mode 100644
index 00000000..6f64c595
--- /dev/null
+++ b/apps/36-blocks-widget/src/app/otp/component/send-otp-center/utility/model.ts
@@ -0,0 +1,6 @@
+export { WidgetVersion, InputFields } from '../../../widget/utility/model';
+
+export enum OtpErrorCodes {
+ VerifyLimitReached = 704,
+ InvalidOtp = 703,
+}
diff --git a/apps/36-blocks-widget/src/app/otp/component/subscription-center/subscription-center.component.html b/apps/36-blocks-widget/src/app/otp/component/subscription-center/subscription-center.component.html
new file mode 100644
index 00000000..7781d887
--- /dev/null
+++ b/apps/36-blocks-widget/src/app/otp/component/subscription-center/subscription-center.component.html
@@ -0,0 +1,125 @@
+
diff --git a/apps/36-blocks-widget/src/app/otp/component/subscription-center/subscription-center.component.scss b/apps/36-blocks-widget/src/app/otp/component/subscription-center/subscription-center.component.scss
new file mode 100644
index 00000000..57d5f6f2
--- /dev/null
+++ b/apps/36-blocks-widget/src/app/otp/component/subscription-center/subscription-center.component.scss
@@ -0,0 +1,398 @@
+// @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&display=swap');
+
+// .container {
+// background: #ffffff !important;
+// padding: 20px;
+// text-align: left;
+// position: relative;
+// height: 100vh;
+// width: 100vw;
+// font-family: 'Outfit', sans-serif;
+// z-index: 1;
+// flex-direction: column;
+// overflow-y: auto;
+// box-sizing: border-box;
+// }
+
+// /* When used in dialog, override the positioning */
+// :host-context(.subscription-center-dialog) .container {
+// position: relative !important;
+// top: auto !important;
+// left: auto !important;
+// right: auto !important;
+// bottom: auto !important;
+// height: 100% !important;
+// width: 100% !important;
+// max-height: 700px !important;
+// max-width: 900px !important;
+// }
+
+// /* Dialog-specific styling for better layout */
+// :host-context(.subscription-center-dialog) {
+// .subscription-plans-container {
+// padding: 10px !important;
+// height: calc(100% - 40px) !important;
+// }
+
+// .plans-grid {
+// justify-content: space-around !important;
+// // align-items: flex-start !important;
+// }
+
+// .plan-card {
+// flex: 0 0 280px !important;
+// margin: 10px !important;
+// }
+// }
+
+// // Subscription Plans Styles
+// .subscription-plans-container {
+// flex: 1;
+// display: flex;
+// flex-direction: column;
+// align-items: stretch;
+// justify-content: flex-start;
+// padding: 20px;
+// min-height: auto;
+// overflow-y: visible;
+// font-family: 'Outfit', sans-serif;
+// }
+
+// .plans-grid {
+// display: flex;
+// flex-direction: row;
+// gap: 20px;
+// width: 100%;
+// max-width: 100%;
+// margin: 0;
+// align-items: flex-start;
+// padding: 0 0 0 20px;
+// overflow-x: auto;
+// // overflow-y: hidden;
+
+// // Custom scrollbar styling
+// &::-webkit-scrollbar {
+// height: 8px;
+// }
+
+// &::-webkit-scrollbar-track {
+// background: #f1f1f1;
+// border-radius: 4px;
+// }
+
+// &::-webkit-scrollbar-thumb {
+// background: #c1c1c1;
+// border-radius: 4px;
+
+// &:hover {
+// background: #a8a8a8;
+// }
+// }
+
+// // Responsive behavior for smaller screens
+// @media (max-width: 1200px) {
+// gap: 15px;
+// padding: 15px;
+// }
+
+// @media (max-width: 768px) {
+// flex-direction: column;
+// align-items: center;
+// gap: 20px;
+// overflow-x: visible;
+// overflow-y: auto;
+// }
+// }
+
+// // Ensure highlighted border is always visible
+// .plan-card.highlighted {
+// border: 2px solid #000000 !important;
+// box-shadow: 0 0 0 0px #000000 !important;
+// }
+
+// .plan-card {
+// background: #ffffff;
+// border: 2px solid #e6e6e6;
+// border-radius: 4px;
+// padding: 26px 24px;
+// transition: all 0.3s ease;
+// box-shadow: none;
+// min-width: 250px;
+// max-width: 350px;
+// width: 350px;
+// flex: 1;
+// display: flex;
+// flex-direction: column;
+// justify-content: flex-start;
+// min-height: auto;
+// max-height: none;
+// overflow: visible;
+// min-height: 348px;
+// font-family: 'Outfit', sans-serif;
+// }
+
+// // Dark mode support for plan cards
+// // :host-context(.dark-theme) .plan-card {
+// // background: var(--color-common-slate);
+// // border: 1px solid var(--color-common-border);
+// // color: var(--color-common-text);
+
+// // &:hover {
+// // transform: translateY(-8px);
+// // box-shadow: none;
+// // }
+
+// // &.popular {
+// // transform: scale(1.02);
+
+// // &:hover {
+// // transform: scale(1.02) translateY(-8px);
+// // }
+// // }
+
+// // &.highlighted {
+// // border: 2px solid #000000 !important;
+// // box-shadow: 0 0 0 0px #000000 !important;
+// // }
+// // }
+
+// // Dark mode support for highlighted cards
+// // :host-context(.dark-theme) .plan-card.highlighted {
+// // border: 2px solid var(--color-common-text) !important;
+// // box-shadow: 0 0 0 2px var(--color-common-text) !important;
+
+// // // Mobile responsive
+// // @media (max-width: 768px) {
+// // min-width: 100%;
+// // max-width: 400px;
+// // width: 100%;
+// // padding: 30px 20px;
+
+// // &.popular {
+// // transform: none;
+
+// // &:hover {
+// // transform: translateY(-8px);
+// // }
+// // }
+// // }
+// // }
+
+// .popular-badge {
+// position: absolute;
+// top: -12px;
+// right: 20px;
+// background: #4d4d4d;
+// color: #ffffff;
+// padding: 6px 16px;
+// border-radius: 20px;
+// font-size: var(--font-size-12);
+// font-weight: 600;
+// text-transform: uppercase;
+// letter-spacing: 0.5px;
+// }
+
+// .plan-title {
+// font-size: var(--font-size-28);
+// font-weight: 700;
+// color: var(--color-common-slate);
+// @media (max-width: 768px) {
+// font-size: 24px;
+// }
+// }
+
+// .plan-price {
+// .price-container {
+// display: flex;
+// align-items: flex-start;
+// gap: 6px;
+// }
+
+// .price-number {
+// font-size: 39px;
+// font-weight: 700;
+// color: #4d4d4d;
+// line-height: 1;
+
+// @media (max-width: 768px) {
+// font-size: 42px;
+// }
+// }
+
+// .price-currency {
+// font-size: 16px;
+// font-weight: 400;
+// color: #666666;
+// line-height: 1;
+// margin-top: 4px;
+// margin-left: 4px;
+
+// @media (max-width: 768px) {
+// font-size: 14px;
+// }
+// }
+
+// .price-period {
+// font-size: 18px;
+// color: #666666;
+// font-weight: 500;
+
+// @media (max-width: 768px) {
+// font-size: 16px;
+// }
+// }
+// }
+
+// .plan-description {
+// .description-text {
+// font-size: 14px;
+// color: #666666;
+// font-style: italic;
+// }
+// }
+
+// .included-resources {
+// .resource-boxes {
+// display: flex;
+// flex-direction: column;
+// gap: 8px;
+// margin-top: 6px;
+// }
+
+// .resource-box {
+// border-radius: 4px;
+// padding: 4px 2px;
+// font-size: 14px;
+// font-weight: 600;
+// color: #4d4d4d;
+// text-align: left;
+// }
+// }
+
+// .section-title {
+// font-size: 18px;
+// font-weight: 600;
+// color: #333333;
+// margin: 0 0 8px 0;
+// }
+
+// .plan-features {
+// list-style: none;
+
+// .feature-item {
+// padding: 4px 0 !important;
+// margin-bottom: 0px !important;
+// color: #4d4d4d;
+// font-size: 14px;
+// font-weight: 600;
+// // padding-left: 20px;
+
+// .feature-icon {
+// font-weight: bold;
+// font-size: 14px;
+// color: #22c55e;
+// }
+// }
+// }
+
+// .plan-button {
+// width: 65%;
+// padding: 6px 6px;
+// border-radius: 4px;
+// font-size: 15px;
+// font-weight: 400;
+// font-family: 'Outfit', sans-serif;
+// cursor: pointer;
+// transition: all 0.3s ease;
+// border: 1px solid;
+// margin-top: auto;
+
+// &.primary {
+// background: #4d4d4d;
+// color: #ffffff;
+// border-color: #4d4d4d;
+// font-weight: 700;
+
+// &:hover {
+// background: #333333;
+// border-color: #333333;
+// }
+// }
+
+// &.secondary {
+// background: #ffffff;
+// color: #4d4d4d;
+// border-color: #4d4d4d;
+
+// &:hover {
+// background: #f8f9fa;
+// }
+// }
+
+// @media (max-width: 768px) {
+// padding: 14px 28px;
+// font-size: 16px;
+// }
+// }
+
+// .plan-button-hidden {
+// padding: 16px 32px;
+// border-radius: 12px;
+// font-size: 18px;
+// font-weight: 600;
+// font-family: 'Outfit', sans-serif;
+// background: #f8f9fa;
+// color: #6c757d;
+// border: 2px solid #e9ecef;
+// margin-top: auto;
+// cursor: not-allowed;
+
+// @media (max-width: 768px) {
+// padding: 14px 28px;
+// font-size: 16px;
+// }
+// }
+
+// .close-dialog {
+// position: absolute;
+// top: 16px;
+// right: 16px;
+// z-index: 1000;
+
+// svg {
+// width: 12px;
+// height: 12px;
+// }
+// }
+
+// .divider {
+// height: 1px;
+// background: #e0e0e0;
+// }
+// :host {
+// min-height: 100px;
+// display: flex;
+// flex-direction: column;
+// justify-content: center;
+// .close-dialog {
+// position: absolute;
+// right: 16px;
+// top: 16px;
+// width: 20px;
+// height: 20px;
+// line-height: 20px;
+// @media only screen and (max-width: 768px) {
+// position: fixed;
+// }
+// }
+// .input-filed-wrapper {
+// display: flex;
+// justify-content: center;
+// flex-direction: column;
+// position: relative;
+// }
+// .login-toggle {
+// margin-top: 24px;
+// font-size: 13px;
+// }
+// }
diff --git a/apps/36-blocks-widget/src/app/otp/component/subscription-center/subscription-center.component.ts b/apps/36-blocks-widget/src/app/otp/component/subscription-center/subscription-center.component.ts
new file mode 100644
index 00000000..bf583359
--- /dev/null
+++ b/apps/36-blocks-widget/src/app/otp/component/subscription-center/subscription-center.component.ts
@@ -0,0 +1,146 @@
+import { ChangeDetectionStrategy, Component, OnInit, inject, input, output } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { SpinnerComponent } from '../../ui/spinner.component';
+import { select, Store } from '@ngrx/store';
+import { IAppState } from '../../store/app.state';
+import { BaseComponent } from '@proxy/ui/base-component';
+import { distinctUntilChanged, Observable, takeUntil } from 'rxjs';
+import { isEqual } from 'lodash-es';
+import { subscriptionPlansData } from '../../store/selectors';
+import { getSubscriptionPlans } from '../../store/actions/otp.action';
+
+// export interface SubscriptionPlan {
+// id: string;
+// title: string;
+// price: string;
+// priceValue: number;
+// currency: string;
+// period: string;
+// buttonText: string;
+// buttonStyle: string;
+// isPopular?: boolean;
+// isSelected?: boolean;
+// features?: string[];
+// notIncludedFeatures?: string[];
+// metrics?: string[];
+// extraFeatures?: string[];
+// status?: string;
+// subscribeButtonLink?: string;
+// subscribeButtonHidden?: boolean;
+// }
+
+@Component({
+ selector: 'proxy-subscription-center',
+ // imports: [CommonModule, SpinnerComponent],
+ templateUrl: './subscription-center.component.html',
+ styleUrls: ['./subscription-center.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class SubscriptionCenterComponent extends BaseComponent implements OnInit {
+ // public referenceId = input();
+ // public closeEvent = output();
+ // public planSelected = output();
+ // public isPreview = input(false);
+ // public togglePopUp = output();
+ // public isLogin = input();
+ // public loginRedirectUrl = input();
+
+ // public subscriptionPlans$: Observable;
+ // public subscriptionPlans: any[] = [];
+
+ // private store = inject>(Store);
+
+ // constructor() {
+ // super();
+
+ // this.subscriptionPlans$ = this.store.pipe(
+ // select(subscriptionPlansData),
+ // distinctUntilChanged(isEqual),
+ // takeUntil(this.destroy$)
+ // );
+ // }
+
+ ngOnInit(): void {
+ // this.subscriptionPlans$.pipe(takeUntil(this.destroy$)).subscribe((res: any) => {
+ // if (res && res.data && Array.isArray(res.data)) {
+ // this.subscriptionPlans = this.formatSubscriptionPlans(res.data);
+ // } else {
+ // this.subscriptionPlans = [];
+ // }
+ // });
+ }
+
+ // private formatSubscriptionPlans(plans: any[]): any[] {
+ // return plans.map((plan, index) => ({
+ // id: plan.planName?.toLowerCase().replace(/\s+/g, '-') || `plan-${index}`,
+ // title: plan.plan_name || 'Unnamed Plan',
+ // price: plan.plan_price || 'Free',
+ // priceValue: this.extractPriceValue(plan.plan_price),
+ // currency: this.extractCurrency(plan.plan_price),
+ // period: 'per month',
+ // buttonText: this.isLogin() ? 'Upgrade' : 'Get Started',
+ // buttonStyle: 'primary',
+ // isPopular: plan.PlanMeta?.highlight_plan || false,
+ // tag: plan.plan_meta?.tag || '',
+ // isSelected: false,
+ // features: plan.plan_meta?.features?.included || [],
+ // notIncludedFeatures: plan.plan_meta?.features?.notIncluded || [],
+ // metrics: plan.plan_meta?.metrics || [],
+ // extraFeatures: plan.plan_meta?.extra || [],
+ // status: 'active',
+ // subscribeButtonLink: this.isLogin()
+ // ? plan.subscribe_button_link?.replace('{ref_id}', this.referenceId())
+ // : this.loginRedirectUrl(),
+ // subscribeButtonHidden: false,
+ // }));
+ // }
+
+ // private extractPriceValue(priceString: string): number {
+ // if (!priceString) return 0;
+ // const match = priceString.match(/[\d.]+/);
+ // return match ? parseFloat(match[0]) : 0;
+ // }
+
+ // private extractCurrency(priceString: string): string {
+ // if (!priceString) return '';
+ // const match = priceString.match(/[A-Z]{3}/);
+ // return match ? match[0] : '';
+ // }
+
+ // private formatCharges(charges: any[]): string[] {
+ // if (!charges || !Array.isArray(charges)) return [];
+ // return charges.map((charge) => {
+ // const quota = charge.quotas || '';
+ // const metricName = charge.billable_metric_name || '';
+ // return `${quota} ${metricName}`.trim();
+ // });
+ // }
+
+ // private getIncludedFeatures(charges: any[]): string[] {
+ // if (!charges || !Array.isArray(charges)) return [];
+ // return charges.map((charge) => {
+ // const quota = charge.quotas || '';
+ // const metricName = charge.billable_metric_name || '';
+ // return `${quota} ${metricName}`.trim();
+ // });
+ // }
+
+ // public close(value: boolean): void {
+ // this.closeEvent.emit(value);
+ // this.togglePopUp.emit();
+ // }
+
+ // public selectPlan(plan: SubscriptionPlan): void {
+ // // Update selection state
+ // this.subscriptionPlans.forEach((p) => (p.isSelected = false));
+ // plan.isSelected = true;
+
+ // // Emit selected plan
+ // this.planSelected.emit(plan);
+
+ // // Navigate to subscribe button link if available
+ // if (plan.subscribeButtonLink) {
+ // window.open(plan.subscribeButtonLink, '_blank');
+ // }
+ // }
+}
diff --git a/apps/36-blocks-widget/src/app/otp/index.ts b/apps/36-blocks-widget/src/app/otp/index.ts
new file mode 100644
index 00000000..96c1dc0f
--- /dev/null
+++ b/apps/36-blocks-widget/src/app/otp/index.ts
@@ -0,0 +1,2 @@
+export { ProxyAuthWidgetComponent } from './widget/widget.component';
+export { OtpModule } from './otp.module';
diff --git a/apps/proxy-auth/src/app/otp/model/otp.ts b/apps/36-blocks-widget/src/app/otp/model/otp.ts
similarity index 92%
rename from apps/proxy-auth/src/app/otp/model/otp.ts
rename to apps/36-blocks-widget/src/app/otp/model/otp.ts
index 0ecadfd2..20cc5e8c 100644
--- a/apps/proxy-auth/src/app/otp/model/otp.ts
+++ b/apps/36-blocks-widget/src/app/otp/model/otp.ts
@@ -1,3 +1,9 @@
+export enum UserManagementTab {
+ Members = 'members',
+ Roles = 'roles',
+ Permissions = 'permissions',
+}
+
export type CreateMutable = {
-readonly [Property in keyof Type]?: Type[Property];
};
diff --git a/apps/36-blocks-widget/src/app/otp/organization-details/organization-details.component.html b/apps/36-blocks-widget/src/app/otp/organization-details/organization-details.component.html
new file mode 100644
index 00000000..ad613b33
--- /dev/null
+++ b/apps/36-blocks-widget/src/app/otp/organization-details/organization-details.component.html
@@ -0,0 +1,305 @@
+
+
+
+
+
+
+
+
+ @if (!isEditing) {
+
+
+
+
+
+
+
+
+
Company Name
+
+ {{ organizationForm.get('companyName')?.value || '—' }}
+
+
+
+
+
+
+
+
+
+
+
Email
+
+ {{ organizationForm.get('email')?.value || '—' }}
+
+
+
+
+
+
+
+
+
+
Phone Number
+
+ {{ organizationForm.get('phoneNumber')?.value || '—' }}
+
+
+
+
+
+
+
+
+
+
+
Timezone
+
+ {{ organizationForm.get('timeZoneName')?.value || '—' }}
+
+
+
+
+ }
+
+
+
+
+ @if (isEditing) {
+
+ }
+
+
diff --git a/apps/36-blocks-widget/src/app/otp/organization-details/organization-details.component.scss b/apps/36-blocks-widget/src/app/otp/organization-details/organization-details.component.scss
new file mode 100644
index 00000000..76c0be11
--- /dev/null
+++ b/apps/36-blocks-widget/src/app/otp/organization-details/organization-details.component.scss
@@ -0,0 +1 @@
+// Styles migrated to Tailwind CSS — no custom classes needed.
diff --git a/apps/proxy-auth/src/app/otp/organization-details/organization-details.component.ts b/apps/36-blocks-widget/src/app/otp/organization-details/organization-details.component.ts
similarity index 66%
rename from apps/proxy-auth/src/app/otp/organization-details/organization-details.component.ts
rename to apps/36-blocks-widget/src/app/otp/organization-details/organization-details.component.ts
index c6bbee37..08b3bed5 100644
--- a/apps/proxy-auth/src/app/otp/organization-details/organization-details.component.ts
+++ b/apps/36-blocks-widget/src/app/otp/organization-details/organization-details.component.ts
@@ -1,29 +1,51 @@
-import { Input, OnDestroy, OnInit } from '@angular/core';
-
-import { Component, ViewEncapsulation } from '@angular/core';
+import {
+ AfterViewInit,
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ ElementRef,
+ OnDestroy,
+ OnInit,
+ ViewChild,
+ effect,
+ inject,
+ input,
+} from '@angular/core';
+import { WidgetPortalRef, WidgetPortalService } from '../service/widget-portal.service';
+import { ToastService } from '../service/toast.service';
+import { ToastComponent } from '../service/toast.component';
+import { WidgetTheme } from '@proxy/constant';
+import { CommonModule } from '@angular/common';
+import { ReactiveFormsModule } from '@angular/forms';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { BaseComponent } from 'libs/ui/base-component/src/lib/base-component/base.component';
import { OtpService } from '../service/otp.service';
+import { WidgetThemeService } from '../service/widget-theme.service';
import { finalize, takeUntil } from 'rxjs';
-import { MatSnackBar } from '@angular/material/snack-bar';
import { EMAIL_REGEX } from '@proxy/regex';
@Component({
selector: 'organization-details',
+ imports: [CommonModule, ReactiveFormsModule, ToastComponent],
templateUrl: './organization-details.component.html',
- encapsulation: ViewEncapsulation.ShadowDom,
- styleUrls: ['../../../styles.scss', './organization-details.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ styleUrls: ['./organization-details.component.scss'],
})
-export class OrganizationDetailsComponent extends BaseComponent implements OnInit, OnDestroy {
- @Input() public authToken: string;
- @Input() public theme: string;
+export class OrganizationDetailsComponent extends BaseComponent implements OnInit, AfterViewInit, OnDestroy {
+ public authToken = input();
+ public theme = input();
+ protected readonly WidgetTheme = WidgetTheme;
+ private readonly themeService = inject(WidgetThemeService);
+ get isDark(): boolean {
+ return this.themeService.isDark(this.theme() as WidgetTheme);
+ }
public organizationForm = new FormGroup({
companyName: new FormControl('', [Validators.required, Validators.minLength(3)]),
email: new FormControl('', [Validators.required, Validators.pattern(EMAIL_REGEX)]),
phoneNumber: new FormControl('', [Validators.pattern(/^$|^[0-9]{10,15}$/)]),
- timezone: new FormControl('', [Validators.required]),
- timeZoneName: new FormControl('', [Validators.required]),
+ timezone: new FormControl(''),
+ timeZoneName: new FormControl(''),
});
public updateInProgress = false;
@@ -46,16 +68,28 @@ export class OrganizationDetailsComponent extends BaseComponent implements OnIni
// Snapshot taken when the user clicks Edit, so Cancel can restore it
private editSnapshot: typeof this.initialFormValue = null;
- constructor(private otpService: OtpService, private snackBar: MatSnackBar) {
+ private otpService = inject(OtpService);
+ private cdr = inject(ChangeDetectorRef);
+ readonly toastService = inject(ToastService);
+ private readonly widgetPortal = inject(WidgetPortalService);
+
+ @ViewChild('editDialogPortal') private editDialogPortalEl?: ElementRef;
+ @ViewChild('toastPortal') private toastPortalEl?: ElementRef;
+
+ private editDialogRef: WidgetPortalRef | null = null;
+ private toastPortalRef: WidgetPortalRef | null = null;
+
+ constructor() {
super();
+ effect(() => this.themeService.setInputTheme(this.theme()));
}
public allowedUpdatePermissions: boolean = false;
ngOnInit(): void {
- if (this.authToken) {
+ if (this.authToken()) {
this.otpService
- .getOrganizationDetails(this.authToken)
+ .getOrganizationDetails(this.authToken())
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (res) => {
@@ -76,13 +110,14 @@ export class OrganizationDetailsComponent extends BaseComponent implements OnIni
};
this.organizationForm.patchValue(value);
this.initialFormValue = value;
+ this.cdr.markForCheck();
}
},
error: () => {},
});
this.otpService
- .getTimezones(this.authToken)
+ .getTimezones(this.authToken())
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (res) => {
@@ -104,19 +139,37 @@ export class OrganizationDetailsComponent extends BaseComponent implements OnIni
});
}
+ ngAfterViewInit(): void {
+ if (this.toastPortalEl?.nativeElement) {
+ this.toastPortalRef = this.widgetPortal.attach(this.toastPortalEl.nativeElement);
+ }
+ }
+
ngOnDestroy(): void {
+ this.editDialogRef?.detach();
+ this.toastPortalRef?.detach();
super.ngOnDestroy();
}
+ private showToast(message: string | undefined, type: 'success' | 'error'): void {
+ if (!message) return;
+ type === 'success' ? this.toastService.success(message) : this.toastService.error(message);
+ }
+
// ── NEW: enter edit mode ──────────────────────────────────────
public startEdit(): void {
- // Snapshot current values so Cancel can restore them
this.editSnapshot = { ...this.organizationForm.value } as typeof this.initialFormValue;
this.isEditing = true;
+ this.cdr.detectChanges();
+ if (this.editDialogPortalEl?.nativeElement) {
+ this.editDialogRef = this.widgetPortal.attach(this.editDialogPortalEl.nativeElement);
+ }
}
// ── NEW: cancel and restore form to pre-edit state ────────────
public cancelEdit(): void {
+ this.editDialogRef?.detach();
+ this.editDialogRef = null;
if (this.editSnapshot) {
this.organizationForm.patchValue(this.editSnapshot);
}
@@ -134,7 +187,7 @@ export class OrganizationDetailsComponent extends BaseComponent implements OnIni
}
public onSubmit(): void {
- if (!this.organizationForm.valid || !this.authToken || this.updateInProgress) {
+ if (!this.organizationForm.valid || !this.authToken() || this.updateInProgress) {
this.organizationForm.markAllAsTouched();
return;
}
@@ -157,13 +210,16 @@ export class OrganizationDetailsComponent extends BaseComponent implements OnIni
this.initialFormValue.timezone === current.timezone &&
this.initialFormValue.timeZoneName === current.timeZoneName
) {
- this.isEditing = false; // just close edit mode silently
+ this.editDialogRef?.detach();
+ this.editDialogRef = null;
+ this.isEditing = false;
return;
}
this.updateInProgress = true;
+ this.cdr.markForCheck();
this.otpService
- .updateCompany(this.authToken, {
+ .updateCompany(this.authToken(), {
name: organizationDetails.companyName ?? '',
email: organizationDetails.email ?? '',
mobile: organizationDetails.phoneNumber ?? '',
@@ -172,18 +228,19 @@ export class OrganizationDetailsComponent extends BaseComponent implements OnIni
})
.pipe(
takeUntil(this.destroy$),
- finalize(() => (this.updateInProgress = false))
+ finalize(() => {
+ this.updateInProgress = false;
+ this.cdr.markForCheck();
+ })
)
.subscribe({
next: (res) => {
this.initialFormValue = { ...current };
- this.isEditing = false; // ← close edit mode on success
- // this.snackBar.open(res?.data?.message ?? 'Information successfully updated', '✕', {
- // duration: 3000,
- // horizontalPosition: 'center',
- // verticalPosition: 'top',
- // panelClass: ['success-snackbar'],
- // });
+ this.editDialogRef?.detach();
+ this.editDialogRef = null;
+ this.isEditing = false;
+ this.showToast(res?.data?.message, 'success');
+ this.cdr.markForCheck();
window.dispatchEvent(
new CustomEvent('organizationDetailsUpdateResponse', {
bubbles: true,
@@ -193,13 +250,8 @@ export class OrganizationDetailsComponent extends BaseComponent implements OnIni
);
},
error: (error) => {
- // Stay in edit mode so user can retry
- // this.snackBar.open('Something went wrong', '✕', {
- // duration: 3000,
- // horizontalPosition: 'center',
- // verticalPosition: 'top',
- // panelClass: ['error-snackbar'],
- // });
+ this.showToast(undefined, 'error');
+ this.cdr.markForCheck();
window.dispatchEvent(
new CustomEvent('organizationDetailsUpdateResponse', {
bubbles: true,
diff --git a/apps/proxy-auth/src/app/otp/service/otp-utility.service.ts b/apps/36-blocks-widget/src/app/otp/service/otp-utility.service.ts
similarity index 100%
rename from apps/proxy-auth/src/app/otp/service/otp-utility.service.ts
rename to apps/36-blocks-widget/src/app/otp/service/otp-utility.service.ts
diff --git a/apps/proxy-auth/src/app/otp/service/otp-widget.service.ts b/apps/36-blocks-widget/src/app/otp/service/otp-widget.service.ts
similarity index 100%
rename from apps/proxy-auth/src/app/otp/service/otp-widget.service.ts
rename to apps/36-blocks-widget/src/app/otp/service/otp-widget.service.ts
diff --git a/apps/proxy-auth/src/app/otp/service/otp.service.ts b/apps/36-blocks-widget/src/app/otp/service/otp.service.ts
similarity index 99%
rename from apps/proxy-auth/src/app/otp/service/otp.service.ts
rename to apps/36-blocks-widget/src/app/otp/service/otp.service.ts
index 20d7df94..7eb1beda 100644
--- a/apps/proxy-auth/src/app/otp/service/otp.service.ts
+++ b/apps/36-blocks-widget/src/app/otp/service/otp.service.ts
@@ -6,7 +6,7 @@ import { map } from 'rxjs/operators';
import { OtpResModel, ISendOtpReq, IRetryOtpReq, IVerifyOtpReq, IWidgetResponse, IGetWidgetData } from '../model/otp';
import { otpVerificationUrls } from './urls/otp-urls';
import { HttpWrapperService } from '@proxy/services/http-wrapper-no-auth';
-import { environment } from 'apps/proxy-auth/src/environments/environment';
+import { environment } from 'apps/36-blocks-widget/src/environments/environment';
@Injectable({
providedIn: 'root',
diff --git a/apps/36-blocks-widget/src/app/otp/service/proxy-auth-dom-builder.service.ts b/apps/36-blocks-widget/src/app/otp/service/proxy-auth-dom-builder.service.ts
new file mode 100644
index 00000000..749004d5
--- /dev/null
+++ b/apps/36-blocks-widget/src/app/otp/service/proxy-auth-dom-builder.service.ts
@@ -0,0 +1,130 @@
+import { Injectable, Renderer2 } from '@angular/core';
+import { WidgetTheme } from '@proxy/constant';
+
+@Injectable({ providedIn: 'root' })
+export class ProxyAuthDomBuilderService {
+ createLogoElement(renderer: Renderer2, logoUrl: string): HTMLElement | null {
+ if (!logoUrl) return null;
+ const wrapper: HTMLElement = renderer.createElement('div');
+ wrapper.style.cssText = 'width:316px;display:flex;justify-content:center;margin:0 8px 12px 8px;';
+ const img: HTMLImageElement = renderer.createElement('img');
+ img.src = logoUrl;
+ img.alt = 'Logo';
+ img.loading = 'lazy';
+ img.style.cssText = 'max-height:48px;max-width:200px;object-fit:contain;';
+ renderer.appendChild(wrapper, img);
+ return wrapper;
+ }
+
+ appendSkeletonLoader(renderer: Renderer2, element: HTMLElement): void {
+ if (element.querySelector('#skeleton-loader')) return;
+ const container = renderer.createElement('div');
+ container.id = 'skeleton-loader';
+ container.style.cssText = 'display:block;width:100%;';
+ if (!document.getElementById('skeleton-animation')) {
+ const style = renderer.createElement('style');
+ style.id = 'skeleton-animation';
+ style.textContent =
+ '@keyframes skeleton-loading{0%{background-position:200% 0}100%{background-position:-200% 0}}';
+ document.head.appendChild(style);
+ }
+ for (let i = 0; i < 3; i++) {
+ const bone = renderer.createElement('div');
+ bone.style.cssText =
+ 'width:230px;height:40px;background:linear-gradient(90deg,#f0f0f0 25%,#e0e0e0 50%,#f0f0f0 75%);background-size:200% 100%;animation:skeleton-loading 1.5s infinite;border-radius:4px;margin:8px 8px 16px 8px;display:block;box-sizing:border-box;';
+ renderer.appendChild(container, bone);
+ }
+ renderer.appendChild(element, container);
+ }
+
+ removeSkeletonLoader(renderer: Renderer2, element: HTMLElement): void {
+ element.querySelectorAll('#skeleton-loader').forEach((loader) => {
+ if (loader.parentNode) renderer.removeChild(element, loader);
+ });
+ this.forceRemoveAllSkeletonLoaders(renderer, element);
+ }
+
+ forceRemoveAllSkeletonLoaders(renderer: Renderer2, referenceElement: HTMLElement | null): void {
+ if (referenceElement) {
+ referenceElement.querySelectorAll('#skeleton-loader').forEach((loader) => {
+ renderer.removeChild(referenceElement, loader);
+ });
+ }
+ document.querySelectorAll('#skeleton-loader').forEach((loader) => {
+ if (loader.parentNode) loader.parentNode.removeChild(loader);
+ });
+ }
+
+ addPasswordVisibilityToggle(
+ renderer: Renderer2,
+ input: HTMLInputElement,
+ container: HTMLElement,
+ theme: string
+ ): void {
+ let visible = false;
+ const iconColor = theme === WidgetTheme.Dark ? '#e5e7eb' : '#5d6164';
+ const toggleBtn: HTMLButtonElement = renderer.createElement('button');
+ toggleBtn.type = 'button';
+ toggleBtn.style.cssText =
+ 'position:absolute;right:12px;top:50%;transform:translateY(-50%);border:none;background:transparent;cursor:pointer;padding:0;margin:0;width:20px;height:20px;display:flex;align-items:center;justify-content:center;z-index:1;';
+ const hiddenIcon = ` `;
+ const visibleIcon = ` `;
+ const renderIcon = () => {
+ toggleBtn.innerHTML = visible ? visibleIcon : hiddenIcon;
+ };
+ renderIcon();
+ toggleBtn.addEventListener('click', () => {
+ visible = !visible;
+ input.type = visible ? 'text' : 'password';
+ renderIcon();
+ });
+ renderer.appendChild(container, toggleBtn);
+ }
+
+ inputStyle(theme: string, borderRadius: string, paddingRight = false): string {
+ const isDark = theme === WidgetTheme.Dark;
+ return `width:100%;height:44px;padding:0 ${paddingRight ? '44px' : '16px'} 0 16px;border:1px solid ${
+ isDark ? '#ffffff' : '#cbd5e1'
+ };border-radius:${borderRadius};background:${isDark ? 'transparent' : '#ffffff'};color:${
+ isDark ? '#ffffff' : '#1f2937'
+ };font-size:14px;outline:none;box-sizing:border-box;`;
+ }
+
+ setInlineError(errorEl: HTMLElement, message: string): void {
+ errorEl.textContent = message;
+ errorEl.style.display = message ? 'block' : 'none';
+ }
+
+ createErrorElement(renderer: Renderer2): HTMLElement {
+ const el: HTMLElement = renderer.createElement('div');
+ el.style.cssText = 'color:#d14343;font-size:14px;min-height:16px;display:none;margin-top:-4px;';
+ return el;
+ }
+
+ createBackButton(renderer: Renderer2): HTMLButtonElement {
+ const btn: HTMLButtonElement = renderer.createElement('button');
+ btn.type = 'button';
+ btn.innerHTML = ` `;
+ btn.style.cssText =
+ 'background:transparent;border:none;cursor:pointer;padding:4px;display:flex;align-items:center;margin-bottom:8px;';
+ return btn;
+ }
+
+ createOrDivider(renderer: Renderer2, primaryColor: string): HTMLElement {
+ const container: HTMLElement = renderer.createElement('div');
+ container.setAttribute('data-or-divider', 'true');
+ container.style.cssText = 'display:flex;align-items:center;margin:8px 8px 12px 8px;width:316px;';
+ const lineStyle = 'flex:1;height:1px;background-color:#e0e0e0;';
+ const left: HTMLElement = renderer.createElement('div');
+ left.style.cssText = lineStyle;
+ const right: HTMLElement = renderer.createElement('div');
+ right.style.cssText = lineStyle;
+ const text: HTMLElement = renderer.createElement('span');
+ text.textContent = 'Or continue with';
+ text.style.cssText = `padding:0 12px;font-size:12px;color:${primaryColor};font-weight:500;letter-spacing:0.5px;`;
+ renderer.appendChild(container, left);
+ renderer.appendChild(container, text);
+ renderer.appendChild(container, right);
+ return container;
+ }
+}
diff --git a/apps/36-blocks-widget/src/app/otp/service/subscription-renderer.service.ts b/apps/36-blocks-widget/src/app/otp/service/subscription-renderer.service.ts
new file mode 100644
index 00000000..bf984cfc
--- /dev/null
+++ b/apps/36-blocks-widget/src/app/otp/service/subscription-renderer.service.ts
@@ -0,0 +1,160 @@
+import { Injectable } from '@angular/core';
+
+@Injectable({ providedIn: 'root' })
+export class SubscriptionRendererService {
+ // ─── Styles injection ─────────────────────────────────────────────────────
+
+ injectSubscriptionStyles(isDark: boolean): void {
+ if (document.getElementById('subscription-styles')) return;
+ const style = document.createElement('style');
+ style.id = 'subscription-styles';
+ style.textContent = this.buildSubscriptionCSS(isDark);
+ document.head.appendChild(style);
+ }
+
+ private buildSubscriptionCSS(isDark: boolean): string {
+ return `
+ @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&display=swap');
+ .position-relative{position:relative!important}
+ .d-flex{display:flex!important}
+ .d-block{display:block!important}
+ .flex-row{flex-direction:row!important}
+ .flex-column{flex-direction:column!important}
+ .align-items-center{align-items:center!important}
+ .align-items-stretch{align-items:stretch!important}
+ .justify-content-start{justify-content:flex-start!important}
+ .w-100{width:100%!important}
+ .p-0{padding:0!important}
+ .py-3{padding-top:1rem!important;padding-bottom:1rem!important}
+ .m-0{margin:0!important}
+ .mt-0{margin-top:0!important}
+ .mb-2{margin-bottom:.5rem!important}
+ .mb-3{margin-bottom:1rem!important}
+ .mb-4{margin-bottom:1.5rem!important}
+ .my-3{margin-top:1rem!important;margin-bottom:1rem!important}
+ .text-left{text-align:left!important}
+ .gap-2{gap:.5rem!important}
+ .gap-3{gap:1rem!important}
+ .gap-4{gap:1.5rem!important}
+
+ .subscription-plans-container{flex:1;display:flex;flex-direction:column;align-items:stretch;justify-content:flex-start;padding:20px;font-family:'Outfit',sans-serif}
+ .plans-grid{gap:20px;max-width:100%;margin:0;align-items:flex-start;padding:20px 0 0 20px;overflow-x:auto;overflow-y:visible}
+ .plans-grid::-webkit-scrollbar{height:8px}
+ .plans-grid::-webkit-scrollbar-track{background:#f1f1f1;border-radius:4px}
+ .plans-grid::-webkit-scrollbar-thumb{background:#c1c1c1;border-radius:4px}
+ @media(max-width:768px){.plans-grid{flex-direction:column;align-items:center;gap:20px;overflow-x:visible;overflow-y:auto}}
+
+ .plan-card{background:${isDark ? 'transparent' : '#ffffff'};border:${
+ isDark ? '1px solid #e6e6e6' : '2px solid #e6e6e6'
+ };border-radius:4px;padding:26px 24px;min-width:290px;max-width:350px;width:350px;flex:1;min-height:348px;font-family:'Outfit',sans-serif;position:relative;margin-top:30px}
+ .plan-card.highlighted{border:${isDark ? '2px solid #ffffff' : '2px solid #000000'}}
+ @media(max-width:768px){.plan-card{min-width:50%;max-width:400px;width:100%;padding:30px 20px}}
+
+ .popular-badge{position:absolute;top:-12px;right:20px;background:#4d4d4d;color:#fff;padding:6px 16px;border-radius:20px;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;z-index:100}
+ .plan-title{font-size:28px;font-weight:700;color:#333}
+ .plan-price .price-number{font-size:39px;font-weight:700;color:#4d4d4d;line-height:1}
+ .plan-price .price-currency{font-size:16px;font-weight:400;color:#666;line-height:1;margin-top:4px;margin-left:4px}
+ .included-resources .resource-box{border-radius:4px;padding:4px 2px;font-size:14px;font-weight:600;color:#4d4d4d;text-align:left}
+ .section-title{font-size:18px;font-weight:600;color:#333;margin:0 0 8px 0}
+ .plan-features{list-style:none}
+ .plan-features .feature-item{padding:4px 0!important;margin-bottom:0!important;color:#4d4d4d;font-size:14px;font-weight:600}
+ .plan-button{width:65%;padding:6px;border-radius:4px;font-size:15px;font-weight:400;font-family:'Outfit',sans-serif;cursor:pointer;transition:all .3s ease;border:1px solid;margin-top:auto}
+ .plan-button.primary{background:#4d4d4d;color:#fff;border-color:#4d4d4d;font-weight:700}
+ .plan-button.primary:hover{background:#333;border-color:#333}
+ .plan-button.plan-button-disabled,.plan-button:disabled{opacity:.7!important;cursor:not-allowed!important;pointer-events:none!important}
+ .divider{height:1px;background:#e0e0e0}
+ *{box-sizing:border-box;font-family:'Inter',sans-serif;-webkit-font-smoothing:antialiased;color:${
+ isDark ? '#ffffff' : ''
+ }!important}
+ `;
+ }
+
+ // ─── HTML string builders ─────────────────────────────────────────────────
+
+ buildContainerHTML(plans: any[], isDark: boolean, isLogin: boolean): string {
+ if (plans.length === 0) {
+ return `No subscription plans available
`;
+ }
+ const plansHTML = plans.map((p) => this.buildPlanCardHTML(p, isDark, isLogin)).join('');
+ return ``;
+ }
+
+ buildPlanCardHTML(plan: any, isDark: boolean, isLogin: boolean): string {
+ const isPopular = plan.plan_meta?.highlight_plan || false;
+ const popularBadge = plan.plan_meta?.tag ? `${plan.plan_meta.tag}
` : '';
+ const priceMatch = plan.plan_price?.match(/(\d+)\s+(.+)/);
+ const priceValue = priceMatch ? priceMatch[1] : '0';
+ const currency = priceMatch ? priceMatch[2] : 'USD';
+ const iconFill = isDark ? '#ffffff' : '#4d4d4d';
+ const isDisabled = !!plan.isSubscribed;
+ const cardClasses = `plan-card d-flex flex-column gap-3 position-relative${
+ isPopular ? ' popular highlighted' : ''
+ }${plan.isSelected ? ' selected' : ''}`;
+ const buttonLabel = isLogin
+ ? plan.isSubscribed
+ ? 'Your current plan'
+ : 'Get ' + plan.plan_name
+ : 'Get Started';
+ const disabledAttrs = isDisabled ? 'disabled aria-disabled="true"' : '';
+ const disabledStyle = isDisabled ? 'cursor:not-allowed;pointer-events:none;' : '';
+
+ return `
+ ${popularBadge}
+
+
${plan.plan_name}
+
+
${buttonLabel}
+
+
+ ${this.buildMetricsHTML(plan)}
+ ${this.buildFeaturesHTML(plan, iconFill)}
+ ${this.buildExtraFeaturesHTML(plan, iconFill)}
+
`;
+ }
+
+ private buildMetricsHTML(plan: any): string {
+ if (!plan.plan_meta?.metrics?.length) return '';
+ const rows = plan.plan_meta.metrics.map((m: string) => `${m}
`).join('');
+ return ``;
+ }
+
+ private buildFeaturesHTML(plan: any, iconFill: string): string {
+ const included: string[] = plan.plan_meta?.features?.included || [];
+ const notIncluded: string[] = plan.plan_meta?.features?.notIncluded || [];
+ if (!included.length && !notIncluded.length) return '';
+
+ const checkSvg = ` `;
+ const crossSvg = ` `;
+
+ const includedItems = included
+ .map(
+ (f: string) =>
+ `${checkSvg} ${f} `
+ )
+ .join('');
+ const notIncludedItems = notIncluded
+ .map(
+ (f: string) =>
+ `${crossSvg} ${f} `
+ )
+ .join('');
+
+ return `Features ${includedItems}${notIncludedItems} `;
+ }
+
+ private buildExtraFeaturesHTML(plan: any, iconFill: string): string {
+ if (!plan.plan_meta?.extra?.length) return '';
+ const starSvg = ` `;
+ const items = plan.plan_meta.extra
+ .map(
+ (f: string) =>
+ ``
+ )
+ .join('');
+ return ``;
+ }
+}
diff --git a/apps/36-blocks-widget/src/app/otp/service/toast.component.ts b/apps/36-blocks-widget/src/app/otp/service/toast.component.ts
new file mode 100644
index 00000000..530bdfec
--- /dev/null
+++ b/apps/36-blocks-widget/src/app/otp/service/toast.component.ts
@@ -0,0 +1,123 @@
+import { ChangeDetectionStrategy, Component, effect, inject, input } from '@angular/core';
+import { WidgetTheme } from '@proxy/constant';
+import { ToastService } from './toast.service';
+import { WidgetThemeService } from './widget-theme.service';
+
+@Component({
+ selector: 'proxy-toast',
+ standalone: true,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+ @if (toastService.toast()) {
+
+
+
+
+
+ @if (toastService.toast()!.type === 'success') {
+
+
+
+ } @else if (toastService.toast()!.type === 'info') {
+
+
+
+ } @else {
+
+
+
+ }
+
+
+
+ {{
+ toastService.toast()!.type === 'success'
+ ? 'Success'
+ : toastService.toast()!.type === 'info'
+ ? 'Info'
+ : 'Error'
+ }}
+
+
+ {{ toastService.toast()!.message }}
+
+
+
+
+
+
+
+ }
+ `,
+})
+export class ToastComponent {
+ readonly theme = input();
+ readonly toastService = inject(ToastService);
+ private readonly themeService = inject(WidgetThemeService);
+ get isDark(): boolean {
+ return this.themeService.isDark(this.theme() as WidgetTheme);
+ }
+
+ constructor() {
+ effect(() => this.themeService.setInputTheme(this.theme()));
+ }
+}
diff --git a/apps/36-blocks-widget/src/app/otp/service/toast.service.ts b/apps/36-blocks-widget/src/app/otp/service/toast.service.ts
new file mode 100644
index 00000000..7418872f
--- /dev/null
+++ b/apps/36-blocks-widget/src/app/otp/service/toast.service.ts
@@ -0,0 +1,36 @@
+import { Injectable, signal } from '@angular/core';
+
+export interface ToastConfig {
+ message: string;
+ type: 'success' | 'error' | 'info';
+ duration?: number;
+}
+
+@Injectable({ providedIn: 'root' })
+export class ToastService {
+ readonly toast = signal(null);
+ private timer: ReturnType | null = null;
+
+ show(config: ToastConfig): void {
+ if (this.timer) clearTimeout(this.timer);
+ this.toast.set(config);
+ this.timer = setTimeout(() => this.dismiss(), config.duration ?? 3000);
+ }
+
+ success(message: string, duration = 3000): void {
+ this.show({ message, type: 'success', duration });
+ }
+
+ error(message: string, duration = 3000): void {
+ this.show({ message, type: 'error', duration });
+ }
+
+ info(message: string, duration = 3000): void {
+ this.show({ message, type: 'info', duration });
+ }
+
+ dismiss(): void {
+ if (this.timer) clearTimeout(this.timer);
+ this.toast.set(null);
+ }
+}
diff --git a/apps/proxy-auth/src/app/otp/service/urls/otp-urls.ts b/apps/36-blocks-widget/src/app/otp/service/urls/otp-urls.ts
similarity index 100%
rename from apps/proxy-auth/src/app/otp/service/urls/otp-urls.ts
rename to apps/36-blocks-widget/src/app/otp/service/urls/otp-urls.ts
diff --git a/apps/36-blocks-widget/src/app/otp/service/user-management-bridge.service.ts b/apps/36-blocks-widget/src/app/otp/service/user-management-bridge.service.ts
new file mode 100644
index 00000000..658587e1
--- /dev/null
+++ b/apps/36-blocks-widget/src/app/otp/service/user-management-bridge.service.ts
@@ -0,0 +1,71 @@
+import { ApplicationRef, EnvironmentInjector, Injectable, inject } from '@angular/core';
+import { Subject } from 'rxjs';
+import { AddUserDialogComponent } from '../user-management/add-user-dialog.component';
+
+export interface OpenAddUserConfig {
+ authToken: string;
+ theme?: string;
+}
+
+/**
+ * Bridges the global `openAddUserDialog` window event to the
+ * UserManagementComponent regardless of whether the component is
+ * currently mounted.
+ *
+ * If UserManagementComponent IS mounted it delegates directly to it.
+ * If it is NOT mounted, a standalone AddUserDialogComponent is created
+ * dynamically and appended directly to document.body — no parent
+ * component required, no z-index issues.
+ *
+ * Event usage from client page:
+ * window.dispatchEvent(new CustomEvent('openAddUserDialog', {
+ * detail: { authToken: 'xxx', theme: 'dark' }
+ * }));
+ */
+@Injectable({ providedIn: 'root' })
+export class UserManagementBridgeService {
+ /** Emits every time `openAddUserDialog` fires while UserManagementComponent IS mounted. */
+ readonly openAddUser$ = new Subject();
+
+ /** Buffered config when event fires before component mounts. */
+ private _pendingConfig: OpenAddUserConfig | null = null;
+
+ private readonly _appRef = inject(ApplicationRef);
+ private readonly _injector = inject(EnvironmentInjector);
+
+ constructor() {
+ if (typeof window === 'undefined') return;
+ window.addEventListener('openAddUserDialog', (e: Event) => {
+ const config: OpenAddUserConfig = {
+ authToken: (e as CustomEvent).detail?.authToken ?? '',
+ theme: (e as CustomEvent).detail?.theme ?? '',
+ };
+
+ if (this.openAddUser$.observed) {
+ // UserManagementComponent is subscribed — deliver to it directly
+ this.openAddUser$.next();
+ } else {
+ // Component not mounted — open standalone dialog immediately
+ this._openStandaloneDialog(config);
+ }
+ });
+ }
+
+ /**
+ * Called by UserManagementComponent in its constructor.
+ * Returns any buffered config so the component can open the dialog on first render.
+ */
+ consumePending(): OpenAddUserConfig | null {
+ const cfg = this._pendingConfig;
+ this._pendingConfig = null;
+ return cfg;
+ }
+
+ private _openStandaloneDialog(config: OpenAddUserConfig): void {
+ // Direct call - no dynamic import to avoid ES6 module chunks in bundle
+ AddUserDialogComponent.open(this._appRef, this._injector, {
+ authToken: config.authToken,
+ theme: config.theme ?? '',
+ });
+ }
+}
diff --git a/apps/36-blocks-widget/src/app/otp/service/widget-portal.service.ts b/apps/36-blocks-widget/src/app/otp/service/widget-portal.service.ts
new file mode 100644
index 00000000..028a47ad
--- /dev/null
+++ b/apps/36-blocks-widget/src/app/otp/service/widget-portal.service.ts
@@ -0,0 +1,116 @@
+import { ApplicationRef, Injectable, Injector, inject } from '@angular/core';
+import { DomPortal, DomPortalOutlet } from '@angular/cdk/portal';
+import { environment } from '../../../environments/environment';
+
+const STYLE_ELEMENT_ID = 'widget-overlay-styles';
+
+/**
+ * Injects the compiled widget styles.css into document.head so that w-*
+ * utility classes work on elements teleported outside the Shadow DOM.
+ *
+ * The CSS is inlined into proxy-auth.js at build time by build-elements.js
+ * and stored in window.__proxyAuth.inlinedStyles. This ensures the widget
+ * works as a single self-contained JS file without external style dependencies.
+ */
+export function ensureAddUserDialogStyles(): void {
+ ensureOverlayStyles();
+}
+
+function ensureOverlayStyles(): void {
+ if (document.getElementById(STYLE_ELEMENT_ID)) return;
+
+ // Get inlined CSS from global object (injected by build-elements.js)
+ const inlinedCSS = (window as any).__proxyAuth?.inlinedStyles;
+
+ if (!inlinedCSS) {
+ console.warn('[proxy-auth] Widget overlay styles not found in bundle. Dialogs may not render correctly.');
+ return;
+ }
+
+ const style = document.createElement('style');
+ style.id = STYLE_ELEMENT_ID;
+ style.textContent = inlinedCSS;
+ document.head.appendChild(style);
+}
+
+/**
+ * Handle returned by WidgetPortalService.attach().
+ * Call detach() to move the element back to its original DOM position.
+ */
+export class WidgetPortalRef {
+ constructor(
+ private readonly _portal: DomPortal,
+ private readonly _outlet: DomPortalOutlet,
+ private readonly _placeholder: Comment
+ ) {}
+
+ detach(): void {
+ if (this._outlet.hasAttached()) {
+ this._outlet.detach();
+ }
+ // Move element back next to its placeholder in the Shadow DOM
+ this._placeholder.parentNode?.insertBefore(this._portal.element, this._placeholder);
+ this._placeholder.parentNode?.removeChild(this._placeholder);
+ this._outlet.dispose();
+ }
+}
+
+/**
+ * Teleports an existing DOM element (already rendered inside Shadow DOM)
+ * to document.body so it escapes any stacking-context constraints imposed
+ * by the client page's container hierarchy.
+ *
+ * This mirrors how Angular CDK Overlay / Angular Material Dialog works
+ * internally — the Angular view stays attached to the original component
+ * tree (all template bindings continue to work), only the DOM node moves.
+ *
+ * Usage in a component:
+ *
+ * @ViewChild('dialogWrap') dialogWrapRef!: ElementRef;
+ * private portalRef: WidgetPortalRef | null = null;
+ *
+ * openDialog() {
+ * this.showDialog.set(true);
+ * // Wait one tick for Angular to render the @if block
+ * setTimeout(() => {
+ * this.portalRef = this.widgetPortal.attach(this.dialogWrapRef.nativeElement);
+ * });
+ * }
+ *
+ * closeDialog() {
+ * this.portalRef?.detach();
+ * this.portalRef = null;
+ * this.showDialog.set(false);
+ * }
+ */
+@Injectable({ providedIn: 'root' })
+export class WidgetPortalService {
+ private readonly _appRef = inject(ApplicationRef);
+ private readonly _injector = inject(Injector);
+
+ /**
+ * Moves `element` from wherever it currently lives (inside Shadow DOM)
+ * to a new host div that is a direct child of document.body.
+ *
+ * Returns a WidgetPortalRef whose detach() moves the element back.
+ */
+ attach(element: HTMLElement): WidgetPortalRef {
+ ensureOverlayStyles();
+
+ // Leave a comment node as a placeholder so we know where to reinsert
+ const placeholder = document.createComment('widget-portal-placeholder');
+ element.parentNode!.insertBefore(placeholder, element);
+
+ // Host container appended directly to body
+ const host = document.createElement('div');
+ host.setAttribute('data-widget-overlay', '');
+ host.classList.add('proxy-widget-portal');
+ document.body.appendChild(host);
+
+ const outlet = new DomPortalOutlet(host, this._appRef, this._injector);
+ const portal = new DomPortal(element);
+ outlet.attach(portal);
+
+ return new WidgetPortalRef(portal, outlet, placeholder);
+ }
+}
diff --git a/apps/36-blocks-widget/src/app/otp/service/widget-theme.service.ts b/apps/36-blocks-widget/src/app/otp/service/widget-theme.service.ts
new file mode 100644
index 00000000..6dc6b021
--- /dev/null
+++ b/apps/36-blocks-widget/src/app/otp/service/widget-theme.service.ts
@@ -0,0 +1,51 @@
+import { Injectable, OnDestroy, signal, computed } from '@angular/core';
+import { WidgetTheme } from '@proxy/constant';
+
+@Injectable()
+export class WidgetThemeService implements OnDestroy {
+ private readonly _systemDark = signal(
+ typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches
+ );
+
+ private readonly _themeOverride = signal(undefined);
+ private readonly _inputTheme = signal(undefined);
+
+ readonly resolvedTheme = computed(() => this._themeOverride() ?? this._inputTheme());
+
+ readonly isDark$ = computed(() => {
+ const t = this._themeOverride() ?? this._inputTheme();
+ if (t === WidgetTheme.Dark) return true;
+ if (t === WidgetTheme.Light) return false;
+ return this._systemDark();
+ });
+
+ readonly isDark = (theme?: WidgetTheme): boolean => {
+ if (theme === WidgetTheme.Dark) return true;
+ if (theme === WidgetTheme.Light) return false;
+ if (theme !== undefined) return this._systemDark();
+ return this.isDark$();
+ };
+
+ private _mediaQueryCleanup?: () => void;
+
+ constructor() {
+ if (typeof window !== 'undefined') {
+ const mq = window.matchMedia('(prefers-color-scheme: dark)');
+ const handler = (e: MediaQueryListEvent) => this._systemDark.set(e.matches);
+ mq.addEventListener('change', handler);
+ this._mediaQueryCleanup = () => mq.removeEventListener('change', handler);
+ }
+ }
+
+ public setInputTheme(theme: string | undefined): void {
+ this._inputTheme.set(theme);
+ }
+
+ public setThemeOverride(theme: string | undefined): void {
+ this._themeOverride.set(theme);
+ }
+
+ ngOnDestroy(): void {
+ this._mediaQueryCleanup?.();
+ }
+}
diff --git a/apps/proxy-auth/src/app/otp/store/actions/index.ts b/apps/36-blocks-widget/src/app/otp/store/actions/index.ts
similarity index 100%
rename from apps/proxy-auth/src/app/otp/store/actions/index.ts
rename to apps/36-blocks-widget/src/app/otp/store/actions/index.ts
diff --git a/apps/proxy-auth/src/app/otp/store/actions/otp.action.ts b/apps/36-blocks-widget/src/app/otp/store/actions/otp.action.ts
similarity index 100%
rename from apps/proxy-auth/src/app/otp/store/actions/otp.action.ts
rename to apps/36-blocks-widget/src/app/otp/store/actions/otp.action.ts
diff --git a/apps/proxy-auth/src/app/otp/store/app.state.ts b/apps/36-blocks-widget/src/app/otp/store/app.state.ts
similarity index 100%
rename from apps/proxy-auth/src/app/otp/store/app.state.ts
rename to apps/36-blocks-widget/src/app/otp/store/app.state.ts
diff --git a/apps/proxy-auth/src/app/otp/store/effects/index.ts b/apps/36-blocks-widget/src/app/otp/store/effects/index.ts
similarity index 100%
rename from apps/proxy-auth/src/app/otp/store/effects/index.ts
rename to apps/36-blocks-widget/src/app/otp/store/effects/index.ts
diff --git a/apps/proxy-auth/src/app/otp/store/effects/otp.effects.ts b/apps/36-blocks-widget/src/app/otp/store/effects/otp.effects.ts
similarity index 94%
rename from apps/proxy-auth/src/app/otp/store/effects/otp.effects.ts
rename to apps/36-blocks-widget/src/app/otp/store/effects/otp.effects.ts
index aedb91f2..533b5cc5 100644
--- a/apps/proxy-auth/src/app/otp/store/effects/otp.effects.ts
+++ b/apps/36-blocks-widget/src/app/otp/store/effects/otp.effects.ts
@@ -1,4 +1,4 @@
-import { Injectable } from '@angular/core';
+import { Injectable, inject } from '@angular/core';
import { errorResolver } from '@proxy/models/root-models';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { of } from 'rxjs';
@@ -7,15 +7,13 @@ import { OtpResModel } from '../../model/otp';
import { OtpService } from '../../service/otp.service';
import { otpActions } from '../actions/index';
import { OtpUtilityService } from '../../service/otp-utility.service';
-import { environment } from 'apps/proxy-auth/src/environments/environment';
+import { environment } from 'apps/36-blocks-widget/src/environments/environment';
@Injectable()
export class OtpEffects {
- constructor(
- private actions$: Actions,
- private otpService: OtpService,
- private otpUtilityService: OtpUtilityService
- ) {}
+ private actions$ = inject(Actions);
+ private otpService = inject(OtpService);
+ private otpUtilityService = inject(OtpUtilityService);
getWidgetData$ = createEffect(() =>
this.actions$.pipe(
@@ -24,20 +22,32 @@ export class OtpEffects {
return this.otpService.getWidgetData(p.referenceId, p.payload).pipe(
map((res: any) => {
if (res) {
+ console.log(
+ '[ProxyAuth] API response received, ciphered length:',
+ res?.data?.ciphered?.length
+ );
+ const decrypted = this.otpUtilityService.aesDecrypt(
+ res?.data?.ciphered ?? '',
+ environment.apiEncodeKey,
+ environment.apiIvKey,
+ true
+ );
+ console.log(
+ '[ProxyAuth] Decrypted result:',
+ decrypted ? 'OK (' + decrypted.length + ' chars)' : 'EMPTY'
+ );
+ const parsed = JSON.parse(decrypted);
+ console.log('[ProxyAuth] Parsed widget data:', parsed);
return otpActions.getWidgetDataComplete({
- response: JSON.parse(
- this.otpUtilityService.aesDecrypt(
- res?.data?.ciphered ?? '',
- environment.apiEncodeKey,
- environment.apiIvKey,
- true
- )
- ),
+ response: parsed,
theme: res?.data,
});
}
}),
catchError((err) => {
+ console.error('[ProxyAuth] getWidgetData failed:', err);
+ console.error('[ProxyAuth] apiEncodeKey defined:', !!environment.apiEncodeKey);
+ console.error('[ProxyAuth] apiIvKey defined:', !!environment.apiIvKey);
return of(
otpActions.getWidgetDataError({
errors: errorResolver(err.errors),
@@ -95,7 +105,6 @@ export class OtpEffects {
});
}),
catchError((err) => {
- console.log('err', err);
return of(
otpActions.verifyOtpActionError({
errors: errorResolver(err.errors),
diff --git a/apps/proxy-auth/src/app/otp/store/reducers/otp.reducer.ts b/apps/36-blocks-widget/src/app/otp/store/reducers/otp.reducer.ts
similarity index 100%
rename from apps/proxy-auth/src/app/otp/store/reducers/otp.reducer.ts
rename to apps/36-blocks-widget/src/app/otp/store/reducers/otp.reducer.ts
diff --git a/apps/proxy-auth/src/app/otp/store/selectors/index.ts b/apps/36-blocks-widget/src/app/otp/store/selectors/index.ts
similarity index 100%
rename from apps/proxy-auth/src/app/otp/store/selectors/index.ts
rename to apps/36-blocks-widget/src/app/otp/store/selectors/index.ts
diff --git a/apps/proxy-auth/src/app/otp/store/selectors/otp.selector.ts b/apps/36-blocks-widget/src/app/otp/store/selectors/otp.selector.ts
similarity index 100%
rename from apps/proxy-auth/src/app/otp/store/selectors/otp.selector.ts
rename to apps/36-blocks-widget/src/app/otp/store/selectors/otp.selector.ts
diff --git a/apps/36-blocks-widget/src/app/otp/ui/confirm-dialog.component.ts b/apps/36-blocks-widget/src/app/otp/ui/confirm-dialog.component.ts
new file mode 100644
index 00000000..06a6ad8e
--- /dev/null
+++ b/apps/36-blocks-widget/src/app/otp/ui/confirm-dialog.component.ts
@@ -0,0 +1,94 @@
+import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';
+
+/**
+ * Reusable destructive confirm dialog.
+ * Usage:
+ *
+ */
+@Component({
+ selector: 'proxy-confirm-dialog',
+ standalone: true,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
+
+
+
+
+ {{ title() }}
+
+
+ {{ message() }}
+
+
+
+
+
+ {{ cancelLabel() }}
+
+
+ {{ confirmLabel() }}
+
+
+
+ `,
+})
+export class ConfirmDialogComponent {
+ readonly title = input('Confirm Action');
+ readonly message = input('Are you sure?');
+ readonly confirmLabel = input('Confirm');
+ readonly cancelLabel = input('Cancel');
+ readonly isDark = input(false);
+
+ readonly confirmed = output();
+ readonly cancelled = output();
+
+ readonly _id = Math.random().toString(36).slice(2, 8);
+}
diff --git a/apps/36-blocks-widget/src/app/otp/ui/index.ts b/apps/36-blocks-widget/src/app/otp/ui/index.ts
new file mode 100644
index 00000000..a1051b0d
--- /dev/null
+++ b/apps/36-blocks-widget/src/app/otp/ui/index.ts
@@ -0,0 +1,3 @@
+export { SpinnerComponent } from './spinner.component';
+export { ProgressBarComponent } from './progress-bar.component';
+export { ConfirmDialogComponent } from './confirm-dialog.component';
diff --git a/apps/36-blocks-widget/src/app/otp/ui/progress-bar.component.ts b/apps/36-blocks-widget/src/app/otp/ui/progress-bar.component.ts
new file mode 100644
index 00000000..4623b60b
--- /dev/null
+++ b/apps/36-blocks-widget/src/app/otp/ui/progress-bar.component.ts
@@ -0,0 +1,38 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+
+/**
+ * Usage:
+ * Indeterminate loading bar, replaces mat-progress-bar.
+ */
+@Component({
+ selector: 'proxy-progress-bar',
+ standalone: true,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+ `,
+ styles: [
+ `
+ @keyframes indeterminate {
+ 0% {
+ transform: translateX(-100%) scaleX(0.3);
+ }
+ 50% {
+ transform: translateX(25%) scaleX(0.6);
+ }
+ 100% {
+ transform: translateX(100%) scaleX(0.3);
+ }
+ }
+ `,
+ ],
+})
+export class ProgressBarComponent {}
diff --git a/apps/36-blocks-widget/src/app/otp/ui/spinner.component.ts b/apps/36-blocks-widget/src/app/otp/ui/spinner.component.ts
new file mode 100644
index 00000000..51edb5aa
--- /dev/null
+++ b/apps/36-blocks-widget/src/app/otp/ui/spinner.component.ts
@@ -0,0 +1,31 @@
+import { ChangeDetectionStrategy, Component, input } from '@angular/core';
+
+/**
+ * Usage:
+ * sizes: sm (16px) | md (24px) | lg (32px)
+ */
+@Component({
+ selector: 'proxy-spinner',
+ standalone: true,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
+
+
+ `,
+})
+export class SpinnerComponent {
+ readonly size = input<'sm' | 'md' | 'lg'>('md');
+
+ sizeClass(): string {
+ return { sm: 'size-4', md: 'size-6', lg: 'size-8' }[this.size()];
+ }
+}
diff --git a/apps/36-blocks-widget/src/app/otp/user-management/add-user-dialog.component.ts b/apps/36-blocks-widget/src/app/otp/user-management/add-user-dialog.component.ts
new file mode 100644
index 00000000..0cb613c9
--- /dev/null
+++ b/apps/36-blocks-widget/src/app/otp/user-management/add-user-dialog.component.ts
@@ -0,0 +1,289 @@
+import {
+ ApplicationRef,
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ ComponentRef,
+ DestroyRef,
+ EnvironmentInjector,
+ OnDestroy,
+ OnInit,
+ computed,
+ createComponent,
+ inject,
+ signal,
+} from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { Store, select } from '@ngrx/store';
+import { distinctUntilChanged } from 'rxjs';
+import { isEqual } from 'lodash-es';
+import { IAppState } from '../store/app.state';
+import { otpActions } from '../store/actions';
+import { rolesData, addUserData } from '../store/selectors';
+import { WidgetTheme } from '@proxy/constant';
+import { ensureAddUserDialogStyles } from '../service/widget-portal.service';
+
+/**
+ * Fully standalone Add-User dialog that mounts itself directly onto
+ * document.body so it is never blocked by a parent stacking context.
+ *
+ * Lifecycle:
+ * 1. Call AddUserDialogComponent.open(appRef, injector, config) to create.
+ * 2. The dialog appends its own host to document.body.
+ * 3. On close the host is removed and the ComponentRef is destroyed.
+ */
+@Component({
+ selector: 'add-user-dialog',
+ standalone: true,
+ imports: [CommonModule, ReactiveFormsModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
+ `,
+})
+export class AddUserDialogComponent implements OnInit, OnDestroy {
+ /** Config passed by the bridge service before the component is attached. */
+ authToken = '';
+ theme = '';
+
+ readonly roles = signal
([]);
+ form!: FormGroup;
+
+ private readonly store = inject>(Store);
+ private readonly fb = inject(FormBuilder);
+ private readonly cdr = inject(ChangeDetectorRef);
+ private readonly destroyRef = inject(DestroyRef);
+
+ /** The host element appended to document.body. */
+ private _hostEl: HTMLDivElement | null = null;
+ /** Self-reference so we can destroy from within. */
+ private _selfRef: ComponentRef | null = null;
+
+ private readonly _systemDark = signal(
+ typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches
+ );
+ readonly isDark = computed(() => {
+ if (this.theme === WidgetTheme.Dark) return true;
+ if (this.theme === WidgetTheme.Light) return false;
+ return this._systemDark();
+ });
+
+ ngOnInit(): void {
+ this.form = this.fb.group({
+ name: ['', Validators.required],
+ email: ['', [Validators.required, Validators.email]],
+ mobileNumber: ['', [Validators.pattern(/^(\+?[1-9]\d{1,14}|[0-9]{10})$/)]],
+ role: [''],
+ });
+
+ // Keep host element dark class in sync
+ if (this._hostEl) {
+ this._hostEl.classList.toggle('dark', this.isDark());
+ }
+
+ // Fetch roles for the dropdown
+ this.store.dispatch(otpActions.getRoles({ authToken: this.authToken, itemsPerPage: 1000 }));
+
+ this.store
+ .pipe(select(rolesData), distinctUntilChanged(isEqual), takeUntilDestroyed(this.destroyRef))
+ .subscribe((res) => {
+ if (res?.data?.data) {
+ this.roles.set(res.data.data);
+ this.cdr.markForCheck();
+ }
+ });
+
+ // Close on success
+ this.store
+ .pipe(select(addUserData), distinctUntilChanged(isEqual), takeUntilDestroyed(this.destroyRef))
+ .subscribe((res) => {
+ if (res) {
+ this.close();
+ }
+ });
+
+ // System dark mode changes
+ if (typeof window !== 'undefined') {
+ const mq = window.matchMedia('(prefers-color-scheme: dark)');
+ const listener = (e: MediaQueryListEvent) => {
+ this._systemDark.set(e.matches);
+ if (this._hostEl) {
+ this._hostEl.classList.toggle('dark', this.isDark());
+ }
+ this.cdr.markForCheck();
+ };
+ mq.addEventListener('change', listener);
+ this.destroyRef.onDestroy(() => mq.removeEventListener('change', listener));
+ }
+ }
+
+ ngOnDestroy(): void {
+ this._hostEl?.remove();
+ this._hostEl = null;
+ }
+
+ save(): void {
+ if (!this.form.valid) {
+ this.form.markAllAsTouched();
+ return;
+ }
+ const v = this.form.value;
+ this.store.dispatch(
+ otpActions.addUser({
+ payload: {
+ user: { name: v.name, email: v.email, mobile: v.mobileNumber || '' },
+ role_id: v.role,
+ },
+ authToken: this.authToken,
+ })
+ );
+ }
+
+ close(): void {
+ if (this._selfRef) {
+ this._selfRef.destroy();
+ this._selfRef = null;
+ }
+ }
+
+ /**
+ * Factory: dynamically creates the component, appends it to body, and returns the ref.
+ */
+ static open(
+ appRef: ApplicationRef,
+ injector: EnvironmentInjector,
+ config: { authToken: string; theme: string }
+ ): ComponentRef {
+ ensureAddUserDialogStyles();
+
+ const host = document.createElement('div');
+ host.setAttribute('data-widget-overlay', '');
+ host.classList.toggle(
+ 'dark',
+ (() => {
+ if (config.theme === WidgetTheme.Dark) return true;
+ if (config.theme === WidgetTheme.Light) return false;
+ return window.matchMedia('(prefers-color-scheme: dark)').matches;
+ })()
+ );
+ document.body.appendChild(host);
+
+ const ref = createComponent(AddUserDialogComponent, {
+ environmentInjector: injector,
+ hostElement: host,
+ });
+
+ ref.instance.authToken = config.authToken;
+ ref.instance.theme = config.theme;
+ ref.instance._hostEl = host;
+ ref.instance._selfRef = ref;
+
+ appRef.attachView(ref.hostView);
+ ref.changeDetectorRef.detectChanges();
+
+ return ref;
+ }
+}
diff --git a/apps/36-blocks-widget/src/app/otp/user-management/user-management.component.html b/apps/36-blocks-widget/src/app/otp/user-management/user-management.component.html
new file mode 100644
index 00000000..2b938b7b
--- /dev/null
+++ b/apps/36-blocks-widget/src/app/otp/user-management/user-management.component.html
@@ -0,0 +1,922 @@
+
+
+ @if (hasMultipleTabs()) {
+
+ }
+
+
+
+
+
+ @if (hasMultipleTabs()) {
+
+
+
+
+
+
+
+
+
+ Members
+
+
+
+
+
+
+ Roles
+
+
+
+
+
+
+ Permissions
+
+
+
+
+
+
+
+
+
+
+
+
+ Members
+
+
+
+
+
+
+ Roles
+
+
+
+
+
+
+ Permissions
+
+
+
+
+ }
+
+
+
+
+ @if (activeTab === UserManagementTab.Members) {
+
+
+
+
+
Team members
+
Manage who has access to this workspace.
+
+ @if (canAddUser) {
+
+
+
+
+ Invite member
+
+ }
+
+
+
+
+
+
+ @if (isUsersLoading && !skipSkeletonLoading) {
+
+ @for (s of [1,2,3]; track s) {
+
+ }
+
+ }
+
+
+ @if (!isUsersLoading || skipSkeletonLoading) { @if (userData.length === 0) {
+
+
+
+
+
No team members yet
+
Invite someone to get started.
+
+ }
+
+
+
+ @for (user of userData; track user; let i = $index) {
+
+
+
+ {{ (user.name || user.email || '?').charAt(0).toUpperCase() }}
+
+
+
+ {{ user.name || '—' }}
+
+
+
+
+
+
+ {{ user.email }}
+
+
+
+
+
+ {{ user.role || 'Member' }}
+
+ @if (canEditUser) {
+
+ Edit, {{ user.name }}
+
+ } @if (canRemoveUser) {
+
+ Remove, {{ user.name }}
+
+ }
+
+
+ }
+
+ }
+
+
+ @if (totalUsers > currentPageSize) {
+
+
+ Showing {{ currentPageIndex * currentPageSize + 1 }} –{{
+ (currentPageIndex + 1) * currentPageSize > totalUsers
+ ? totalUsers
+ : (currentPageIndex + 1) * currentPageSize
+ }}
+ of {{ totalUsers }}
+
+
+
+ Previous
+
+ = totalUsers"
+ class="w-btn-secondary"
+ >
+ Next
+
+
+
+ }
+
+ }
+
+
+ @if (activeTab === UserManagementTab.Roles && pass()) {
+
+
+
+
+
Roles
+
Define roles and their associated permissions.
+
+
+
+
+
+ Add role
+
+
+
+
+
+
+
+ @if (isRolesLoading) {
+
+ @for (s of [1,2,3]; track s) {
+
+
+
+ @for (t of [1,2,3,4,5]; track t) {
+
+ }
+
+
+ }
+
+ }
+
+
+ @if (!isRolesLoading) { @if (filteredRolesData.length === 0) {
+
No roles found
+ }
+
+ @for (role of filteredRolesData; track role; let i = $index) {
+
+
+
+ {{
+ role.name
+ }}
+ @if (role.is_default) {
+ Default
+ Default
+ }
+
+
+ Edit
+
+
+ @if (role.c_permissions?.length) {
+
+ @for (p of role.c_permissions; track p.id; let pi = $index) {
+
+
+
+
+ {{ p.name }}
+
+ }
+
+ } @else {
+
No permissions assigned
+ }
+
+ }
+
+ }
+
+ }
+
+
+ @if (activeTab === UserManagementTab.Permissions && pass()) {
+
+
+
+
+
Permissions
+
Granular access controls that can be assigned to roles.
+
+
+
+
+
+ Add permission
+
+
+
+
+
+
+
+ @if (isPermissionsLoading) {
+
+ @for (s of [1,2,3,4,5]; track s) {
+
+ }
+
+ }
+
+
+ @if (!isPermissionsLoading) { @if (filteredPermissionsData.length === 0) {
+
No permissions found
+ }
+
+ @for (permission of filteredPermissionsData; track permission; let i = $index) {
+
+
+
+
+ {{ permission.name }}
+
+
+
+ Edit
+
+
+ }
+
+ }
+
+ }
+
+
+
+
+
+ @if (showDialog()) {
+
+
+
+
+
+
+
+
+
+ @if (isEditRole) {
+
+ }
+
+
+ @if (isEditPermission) {
+
+
+
Permission name *
+
+ @if (addPermissionTabForm.get('permission')?.touched &&
+ addPermissionTabForm.get('permission')?.hasError('required')) {
+
Permission name is required
+ }
+
+
+ Description
+ (optional)
+
+
+
+ }
+
+
+ @if (isEditUser || (!isEditRole && !isEditPermission)) {
+
+
+
+
Full name *
+
+ @if (addUserForm.get('name')?.touched && addUserForm.get('name')?.hasError('required')) {
+
Name is required
+ }
+
+
+
Role
+
+
+ Select role
+ @for (role of roles; track role.id) {
+ {{ role.name }}
+ }
+
+
+
+
+
+
+
Email address *
+
+ @if (addUserForm.get('email')?.touched && addUserForm.get('email')?.hasError('required')) {
+
Email is required
+ } @if (addUserForm.get('email')?.touched && addUserForm.get('email')?.hasError('email')) {
+
Enter a valid email address
+ }
+
+
+
+ Mobile (optional)
+
+
+ @if (addUserForm.get('mobileNumber')?.touched &&
+ addUserForm.get('mobileNumber')?.hasError('pattern')) {
+
+ Enter a valid mobile with country code
+
+ }
+
+ Include country code, e.g. 917001002003
+
+
+ @if (isEditUser && getAvailableAdditionalPermissions().length > 0) {
+
+
+
+ @for (p of getAvailableAdditionalPermissions(); track p.id) {
+
+
+ {{ p.name }}
+
+ }
+
+
+ }
+
+ }
+
+
+
+
+
+
+ }
+
+
+ @if (showConfirmDialog()) {
+
+
+
+
+
+
+
+ Remove member
+
+
+ Are you sure you want to remove
+ {{ pendingDeleteUser?.name }} ? This action cannot be undone.
+
+
+
+
+ Cancel
+ Remove
+
+
+
+ }
+
+
diff --git a/apps/36-blocks-widget/src/app/otp/user-management/user-management.component.scss b/apps/36-blocks-widget/src/app/otp/user-management/user-management.component.scss
new file mode 100644
index 00000000..403805bd
--- /dev/null
+++ b/apps/36-blocks-widget/src/app/otp/user-management/user-management.component.scss
@@ -0,0 +1,153 @@
+// ── Hover-reveal actions (cannot be done in Tailwind without JS) ─────────────
+.hover-actions {
+ opacity: 0;
+ transition: opacity 0.15s ease;
+ position: absolute;
+ top: 6px;
+ right: 12px;
+ z-index: 10;
+}
+
+.table-row:hover .hover-actions {
+ opacity: 1;
+}
+
+.user-item:hover .user-actions {
+ opacity: 1;
+ visibility: visible;
+}
+
+// ── Skeleton shimmer (keyframe animation — not in Tailwind) ───────────────────
+@keyframes shimmer {
+ 0% {
+ background-position: 200% 0;
+ }
+ 100% {
+ background-position: -200% 0;
+ }
+}
+
+.skeleton-avatar,
+.skeleton-text,
+.skeleton-button {
+ border-radius: 4px;
+ background: linear-gradient(
+ 90deg,
+ var(--proxy-neutral-90) 25%,
+ var(--proxy-neutral-95) 50%,
+ var(--proxy-neutral-90) 75%
+ );
+ background-size: 200% 100%;
+ animation: shimmer 1.5s infinite;
+}
+
+.skeleton-avatar {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+}
+
+.skeleton-name {
+ width: 120px;
+ height: 16px;
+ margin-bottom: 6px;
+}
+.skeleton-email {
+ width: 180px;
+ height: 14px;
+}
+.skeleton-role {
+ width: 80px;
+ height: 14px;
+}
+.skeleton-button {
+ width: 60px;
+ height: 32px;
+}
+
+// .dark-theme .skeleton-avatar,
+// .dark-theme .skeleton-text,
+// .dark-theme .skeleton-button {
+// background: linear-gradient(
+// 90deg,
+// var(--proxy-neutral-30) 25%,
+// var(--proxy-neutral-20) 50%,
+// var(--proxy-neutral-30) 75%
+// );
+// background-size: 200% 100%;
+// animation: shimmer 1.5s infinite;
+// }
+
+// ── Angular Material table cell resets ───────────────────────────────────────
+table {
+ box-shadow: none !important;
+}
+
+th,
+td {
+ border: 1px solid var(--proxy-neutral-90);
+ padding: 14px;
+ text-align: left;
+}
+td.mat-cell {
+ padding: 10px;
+}
+th {
+ font-weight: 600;
+ height: 22px;
+ padding-left: 12px !important;
+}
+td {
+ font-size: 12px;
+ height: 22px;
+}
+
+.role-column {
+ width: 25% !important;
+}
+.permissions-column {
+ width: 70% !important;
+ overflow: visible;
+ position: relative;
+}
+.permission-column {
+ overflow: visible;
+}
+.permissions-cell-content {
+ min-height: 40px;
+ padding-right: 60px;
+}
+
+// ── ::ng-deep for Material overlay/tooltip (cannot scope otherwise) ───────────
+::ng-deep .btn-danger {
+ background-color: var(--proxy-error-40) !important;
+ color: var(--proxy-neutral-100) !important;
+
+ &:hover,
+ &:focus {
+ background-color: var(--proxy-error-40) !important;
+ filter: brightness(0.85);
+ }
+}
+
+::ng-deep .permissions-tooltip,
+::ng-deep .email-tooltip {
+ background-color: rgba(0, 0, 0, 0.9) !important;
+ color: var(--proxy-neutral-100) !important;
+ font-size: 12px !important;
+ white-space: pre-line !important;
+ border-radius: 4px !important;
+ padding: 10px !important;
+ line-height: 1.4 !important;
+ text-align: left !important;
+}
+
+// ── Mobile overrides for mat-form-field hint ──────────────────────────────────
+.mobile-number-field {
+ @media (max-width: 600px) {
+ margin-bottom: 16px;
+ }
+ @media (max-width: 380px) {
+ margin-bottom: 30px;
+ }
+}
diff --git a/apps/36-blocks-widget/src/app/otp/user-management/user-management.component.ts b/apps/36-blocks-widget/src/app/otp/user-management/user-management.component.ts
new file mode 100644
index 00000000..a0a24428
--- /dev/null
+++ b/apps/36-blocks-widget/src/app/otp/user-management/user-management.component.ts
@@ -0,0 +1,808 @@
+import {
+ AfterViewInit,
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ DestroyRef,
+ ElementRef,
+ OnDestroy,
+ OnInit,
+ ViewChild,
+ computed,
+ effect,
+ inject,
+ input,
+ signal,
+} from '@angular/core';
+import { WidgetPortalRef, WidgetPortalService } from '../service/widget-portal.service';
+import { UserManagementBridgeService } from '../service/user-management-bridge.service';
+import { CommonModule } from '@angular/common';
+import { ToastService } from '../service/toast.service';
+import { ToastComponent } from '../service/toast.component';
+import { WidgetTheme } from '@proxy/constant';
+import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { select, Store } from '@ngrx/store';
+import { IAppState } from '../store/app.state';
+import { otpActions } from '../store/actions';
+import { debounceTime, distinctUntilChanged, Subject } from 'rxjs';
+import {
+ addUserData,
+ companyUsersData,
+ companyUsersDataInProcess,
+ permissionCreateData,
+ permissionData,
+ roleCreateData,
+ rolesData,
+ updateCompanyUserData,
+ updatePermissionData,
+ updateRoleData,
+ updateUserPermissionData,
+ updateUserRoleData,
+} from '../store/selectors';
+import { isEqual } from 'lodash-es';
+import { WidgetThemeService } from '../service/widget-theme.service';
+import { UserData, Role, UserManagementTab } from '../model/otp';
+
+interface PageEvent {
+ pageIndex: number;
+ pageSize: number;
+ length: number;
+}
+
+@Component({
+ selector: 'user-management',
+ imports: [CommonModule, FormsModule, ReactiveFormsModule, ToastComponent],
+ templateUrl: './user-management.component.html',
+ styleUrls: ['./user-management.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class UserManagementComponent implements OnInit, AfterViewInit, OnDestroy {
+ readonly userToken = input();
+ readonly pass = input(false);
+ readonly theme = input();
+ protected readonly WidgetTheme = WidgetTheme;
+ protected readonly UserManagementTab = UserManagementTab;
+ protected readonly ariaCurrent = ['p', 'a', 'g', 'e'].join('');
+ readonly exclude_role_ids = input([]);
+ readonly include_role_ids = input([]);
+ readonly isHidden = signal(false);
+ readonly showDialog = signal(false);
+ readonly showConfirmDialog = signal(false);
+ private readonly themeService = inject(WidgetThemeService);
+ get isDark(): boolean {
+ return this.themeService.isDark(this.theme() as WidgetTheme);
+ }
+ readonly availableTabs = computed(() => {
+ const tabs = [UserManagementTab.Members];
+ if (this.pass()) {
+ tabs.push(UserManagementTab.Roles, UserManagementTab.Permissions);
+ }
+ return tabs;
+ });
+ readonly hasMultipleTabs = computed(() => this.availableTabs().length > 1);
+ public pendingDeleteUser: any = null;
+ private pendingDeleteIndex: number = -1;
+ private pendingEditUser: UserData | null = null;
+ public roles: any[] = [];
+ public permissions: any[] = [];
+ public searchTerm: string = '';
+ private searchSubject = new Subject();
+
+ public roleSearchTerm: string = '';
+ public filteredRolesData: any[] = [];
+ public defaultRoles: any;
+
+ public permissionSearchTerm: string = '';
+ public filteredPermissionsData: any[] = [];
+ public emailVisibility: { [key: number]: boolean } = {};
+ public expandedRoles: { [key: number]: boolean } = {};
+ public addUserForm: FormGroup;
+ public addRoleForm: FormGroup;
+ public addPermissionTabForm: FormGroup;
+ public isEditRole: boolean = false;
+ public isEditPermission: boolean = false;
+ public isEditUser: boolean = false;
+ public currentEditingUser: UserData | null = null;
+ public currentEditingPermission: UserData | null = null;
+ public userData: any[] = [];
+ public canRemoveUser: boolean = false;
+ public canEditUser: boolean = false;
+ public canAddUser: boolean = false;
+ public totalUsers: number = 0;
+ public currentPageIndex: number = 0;
+ public currentPageSize: number = 50;
+ public isUsersLoading: boolean = true;
+ public isRolesLoading: boolean = false;
+ public isPermissionsLoading: boolean = false;
+ public skipSkeletonLoading: boolean = false;
+ public activeTab: UserManagementTab = UserManagementTab.Members;
+ private readonly destroyRef = inject(DestroyRef);
+ private readonly fb = inject(FormBuilder);
+ private readonly cdr = inject(ChangeDetectorRef);
+ private readonly store = inject>(Store);
+ readonly toastService = inject(ToastService);
+ private readonly widgetPortal = inject(WidgetPortalService);
+ private readonly bridge = inject(UserManagementBridgeService);
+
+ @ViewChild('mainDialogPortal') private mainDialogPortalEl?: ElementRef;
+ @ViewChild('confirmDialogPortal') private confirmDialogPortalEl?: ElementRef;
+ @ViewChild('toastPortal') private toastPortalEl?: ElementRef;
+
+ private mainDialogRef: WidgetPortalRef | null = null;
+ private confirmDialogRef: WidgetPortalRef | null = null;
+ private toastPortalRef: WidgetPortalRef | null = null;
+
+ constructor() {
+ effect(() => this.themeService.setInputTheme(this.theme()));
+ this.store
+ .pipe(select(rolesData), distinctUntilChanged(isEqual), takeUntilDestroyed(this.destroyRef))
+ .subscribe((res) => {
+ if (res) {
+ this.roles = res.data?.data;
+ this.defaultRoles = res.data?.default_roles;
+ this.filteredRolesData = [...this.roles];
+ this.isRolesLoading = false;
+ if (this.pendingEditUser) {
+ this.patchEditUserForm(this.pendingEditUser);
+ this.pendingEditUser = null;
+ }
+ this.cdr.markForCheck();
+ }
+ });
+
+ this.store
+ .pipe(select(permissionData), distinctUntilChanged(isEqual), takeUntilDestroyed(this.destroyRef))
+ .subscribe((res) => {
+ if (res) {
+ this.permissions = res.data.data;
+ this.filteredPermissionsData = [...this.permissions];
+ this.isPermissionsLoading = false;
+ this.cdr.markForCheck();
+ }
+ });
+
+ this.store
+ .pipe(select(companyUsersData), distinctUntilChanged(isEqual), takeUntilDestroyed(this.destroyRef))
+ .subscribe((res) => {
+ if (res) {
+ this.totalUsers = res.data?.totalEntityCount || 0;
+ this.canRemoveUser = res.data?.permissionToRemoveUser;
+ this.canAddUser = res.data?.permissionToAddUser;
+ this.canEditUser = res.data?.permissionToEditUser;
+ this.userData = res.data?.users || [];
+ const pageNo = res.data?.pageNo;
+ const itemsPerPage = res.data?.itemsPerPage;
+ if (pageNo !== undefined) {
+ this.currentPageIndex = pageNo - 1;
+ }
+ if (itemsPerPage !== undefined) {
+ this.currentPageSize = parseInt(itemsPerPage, 10) || 10;
+ }
+ this.cdr.markForCheck();
+ }
+ });
+
+ this.store
+ .pipe(select(companyUsersDataInProcess), takeUntilDestroyed(this.destroyRef))
+ .subscribe((isLoaded) => {
+ this.isUsersLoading = !isLoaded;
+ this.cdr.markForCheck();
+ });
+
+ this.store
+ .pipe(select(addUserData), distinctUntilChanged(isEqual), takeUntilDestroyed(this.destroyRef))
+ .subscribe((res) => {
+ if (res) {
+ this.getCompanyUsers();
+ if (res?.data?.message) this.toastService.success(res.data.message);
+ this.cdr.markForCheck();
+ }
+ });
+
+ this.store
+ .pipe(select(roleCreateData), distinctUntilChanged(isEqual), takeUntilDestroyed(this.destroyRef))
+ .subscribe((res) => {
+ if (res) {
+ this.getRoles();
+ this.refreshFormData();
+ if (res?.data?.message) this.toastService.success(res.data.message);
+ this.cdr.markForCheck();
+ }
+ });
+
+ this.store
+ .pipe(select(permissionCreateData), distinctUntilChanged(isEqual), takeUntilDestroyed(this.destroyRef))
+ .subscribe((res) => {
+ if (res) {
+ this.getPermissions();
+ this.refreshFormData();
+ if (res?.data?.message) this.toastService.success(res.data.message);
+ this.cdr.markForCheck();
+ }
+ });
+
+ this.store
+ .pipe(select(updatePermissionData), distinctUntilChanged(isEqual), takeUntilDestroyed(this.destroyRef))
+ .subscribe((res) => {
+ if (res) {
+ this.getPermissions();
+ this.refreshFormData();
+ if (res?.data?.message) this.toastService.success(res.data.message);
+ this.cdr.markForCheck();
+ }
+ });
+
+ this.store
+ .pipe(select(updateRoleData), distinctUntilChanged(isEqual), takeUntilDestroyed(this.destroyRef))
+ .subscribe((res) => {
+ if (res) {
+ this.getRoles();
+ this.refreshFormData();
+ if (res?.data?.message) this.toastService.success(res.data.message);
+ this.cdr.markForCheck();
+ }
+ });
+
+ this.store
+ .pipe(select(updateCompanyUserData), distinctUntilChanged(isEqual), takeUntilDestroyed(this.destroyRef))
+ .subscribe((res) => {
+ if (res) {
+ this.getCompanyUsers();
+ this.skipSkeletonLoading = false;
+ this.cdr.markForCheck();
+ }
+ });
+
+ this.store
+ .pipe(select(updateUserRoleData), distinctUntilChanged(isEqual), takeUntilDestroyed(this.destroyRef))
+ .subscribe((res) => {
+ if (res) {
+ this.getCompanyUsers();
+ if (res?.data?.message) this.toastService.success(res.data.message);
+ this.cdr.markForCheck();
+ }
+ });
+
+ this.store
+ .pipe(select(updateUserPermissionData), distinctUntilChanged(isEqual), takeUntilDestroyed(this.destroyRef))
+ .subscribe((res) => {
+ if (res) {
+ this.getCompanyUsers();
+ if (res?.data?.message) this.toastService.success(res.data.message);
+ this.cdr.markForCheck();
+ }
+ });
+
+ this.searchSubject
+ .pipe(debounceTime(300), distinctUntilChanged(), takeUntilDestroyed(this.destroyRef))
+ .subscribe((searchTerm) => {
+ this.getCompanyUsers(searchTerm);
+ this.cdr.markForCheck();
+ });
+
+ this.addUserForm = this.fb.group({
+ name: ['', Validators.required],
+ email: ['', [Validators.required, Validators.email]],
+ mobileNumber: ['', [Validators.pattern(/^(\+?[1-9]\d{1,14}|[0-9]{10})$/)]],
+ role: [''],
+ permission: [[]],
+ });
+
+ // Subscribe to role changes to update permissions
+ this.addUserForm
+ .get('role')
+ ?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe((roleId) => {
+ this.onRoleChange(roleId);
+ });
+
+ this.addRoleForm = this.fb.group({
+ roleName: ['', Validators.required],
+ description: [''],
+ permission: [[], Validators.required],
+ });
+
+ this.addPermissionTabForm = this.fb.group({
+ permission: ['', Validators.required],
+ description: [''],
+ });
+ const showMgmt = () => this.isHidden.set(false);
+ const hideMgmt = () => this.isHidden.set(true);
+
+ // openAddUserDialog is handled via UserManagementBridgeService
+ // (works even when the event fires before this component mounts)
+ this.bridge.openAddUser$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => this.addUser());
+
+ // Consume any event that was buffered before this component mounted
+ if (this.bridge.consumePending() !== null) {
+ Promise.resolve().then(() => this.addUser());
+ }
+
+ window.addEventListener('showUserManagement', showMgmt);
+ window.addEventListener('hideUserManagement', hideMgmt);
+
+ this.destroyRef.onDestroy(() => {
+ window.removeEventListener('showUserManagement', showMgmt);
+ window.removeEventListener('hideUserManagement', hideMgmt);
+ this.mainDialogRef?.detach();
+ this.confirmDialogRef?.detach();
+ this.toastPortalRef?.detach();
+ this.showDialog.set(false);
+ this.showConfirmDialog.set(false);
+ });
+ }
+
+ ngOnInit(): void {
+ this.getCompanyUsers();
+ }
+
+ ngAfterViewInit(): void {
+ // Teleport the toast to body immediately (always visible, not conditional)
+ if (this.toastPortalEl?.nativeElement) {
+ this.toastPortalRef = this.widgetPortal.attach(this.toastPortalEl.nativeElement);
+ }
+ }
+
+ ngOnDestroy(): void {
+ // handled via destroyRef.onDestroy above
+ }
+
+ public tabChange(tab: UserManagementTab): void {
+ this.activeTab = tab;
+ if (tab === UserManagementTab.Roles) {
+ this.isRolesLoading = true;
+ this.getRoles();
+ } else if (tab === UserManagementTab.Permissions) {
+ this.isPermissionsLoading = true;
+ this.getPermissions();
+ } else if (tab === UserManagementTab.Members) {
+ this.getCompanyUsers();
+ }
+ }
+
+ public deleteUser(user: any, index: number): void {
+ this.pendingDeleteUser = user;
+ this.pendingDeleteIndex = index;
+ this.showConfirmDialog.set(true);
+ this.cdr.detectChanges();
+ if (this.confirmDialogPortalEl?.nativeElement) {
+ this.confirmDialogRef = this.widgetPortal.attach(this.confirmDialogPortalEl.nativeElement);
+ }
+ }
+
+ public confirmDelete(): void {
+ if (this.pendingDeleteUser) {
+ const userId = (this.pendingDeleteUser as any).user_id;
+ this.userData = this.userData.filter((u: any) => u.user_id !== userId);
+ this.totalUsers = Math.max(0, this.totalUsers - 1);
+ this.store.dispatch(otpActions.deleteUser({ companyId: userId, authToken: this.userToken() }));
+ }
+ this.cancelDelete();
+ }
+
+ public cancelDelete(): void {
+ this.confirmDialogRef?.detach();
+ this.confirmDialogRef = null;
+ this.pendingDeleteUser = null;
+ this.pendingDeleteIndex = -1;
+ this.showConfirmDialog.set(false);
+ }
+
+ public editUser(user: UserData, index: number): void {
+ this.skipSkeletonLoading = true;
+ this.isEditUser = true;
+ this.isEditRole = false;
+ this.isEditPermission = false;
+ this.currentEditingUser = user;
+ this.pendingEditUser = user;
+ this.getRoles();
+ this.openDialog();
+ }
+
+ private patchEditUserForm(user: UserData): void {
+ const roleId = this.getRoleIdByName(user.role);
+ const userPermissionIds = this.getPermissionIdsByName(user.additionalpermissions || []);
+ this.addUserForm.patchValue({
+ name: user.name,
+ email: user.email,
+ mobileNumber: (user as any).mobile || '',
+ role: roleId ? roleId.toString() : user.role || '',
+ permission: userPermissionIds,
+ });
+ this.cdr.markForCheck();
+ }
+
+ public getPermissionsTooltip(user: UserData): string {
+ let tooltip = user?.permissions?.length
+ ? `Permissions:\n• ${user.permissions.join('\n• ')}`
+ : 'No permissions assigned';
+ if (user?.additionalpermissions?.length) {
+ tooltip += `\n\nAdditional Permissions:\n+ ${user.additionalpermissions.join('\n+ ')}`;
+ }
+ return tooltip;
+ }
+
+ public applyFilter(): void {
+ this.searchSubject.next(this.searchTerm);
+ }
+
+ public clearSearch(): void {
+ this.searchTerm = '';
+ this.applyFilter();
+ }
+
+ public isEmailVisible(index: number): boolean {
+ return this.emailVisibility[index] || false;
+ }
+
+ public toggleEmailVisibility(index: number): void {
+ this.emailVisibility[index] = !this.emailVisibility[index];
+ }
+
+ public getRoleIdByName(roleName: string): number | undefined {
+ return this.roles?.find((r) => r.name === roleName)?.id;
+ }
+
+ public getRoleNameById(roleId: number): string {
+ return (roleId && this.roles?.find((r) => r.id === roleId)?.name) || '';
+ }
+
+ public onRoleChange(roleId: number): void {
+ const perms = roleId ? this.roles.find((r) => r.id === roleId)?.c_permissions?.map((p: any) => p.id) ?? [] : [];
+ this.addUserForm.get('permission')?.setValue(perms);
+ }
+
+ public getPermissionIdsByName(permissionNames: string[]): number[] {
+ return permissionNames
+ .map((permissionName) => {
+ const permission = this.permissions.find((p) => p.name === permissionName);
+ return permission?.id;
+ })
+ .filter((id) => id !== undefined) as number[];
+ }
+
+ public getPermissionNamesByIds(permissionIds: number[]): string[] {
+ return permissionIds
+ .map((permissionId) => {
+ const permission = this.permissions.find((p) => p.id === permissionId);
+ return permission?.name;
+ })
+ .filter((name) => name !== undefined) as string[];
+ }
+
+ private openDialog(): void {
+ this.showDialog.set(true);
+ this.cdr.detectChanges();
+ if (this.mainDialogPortalEl?.nativeElement) {
+ this.mainDialogRef = this.widgetPortal.attach(this.mainDialogPortalEl.nativeElement);
+ }
+ }
+
+ public addUser(): void {
+ // Call Add role api to get role to show in dropdown list
+ this.getRoles();
+ this.isEditUser = false;
+ this.isEditRole = false;
+ this.isEditPermission = false;
+ this.currentEditingUser = null;
+ this.addUserForm.reset();
+
+ if (this.defaultRoles?.default_member_role) {
+ this.addUserForm.patchValue({ role: this.defaultRoles.default_member_role });
+ }
+
+ this.openDialog();
+ }
+
+ public closeDialog(): void {
+ this.mainDialogRef?.detach();
+ this.mainDialogRef = null;
+ this.showDialog.set(false);
+ this.isEditUser = false;
+ this.isEditRole = false;
+ this.isEditPermission = false;
+ this.currentEditingUser = null;
+ this.currentEditingPermission = null;
+ }
+
+ public saveUser(): void {
+ if (this.addUserForm.valid) {
+ const formValue = this.addUserForm.value;
+ const selectedRole = formValue.role ? this.getRoleById(formValue.role) : null;
+ const roleName = selectedRole?.name || formValue.role || 'User';
+
+ if ((this.isEditUser || this.isEditRole) && this.currentEditingUser) {
+ // Update existing user
+ const userIndex = this.userData.findIndex((u) => u.userId === this.currentEditingUser!.userId);
+ if (userIndex !== -1) {
+ const originalMobile = (this.currentEditingUser as any).mobile || '';
+ const newMobile = formValue.mobileNumber || '';
+
+ // Build user object with only changed fields
+ const userPayload: any = {
+ id: (this.currentEditingUser as any).user_id,
+ name: formValue.name,
+ };
+
+ // Only include mobile if it has changed
+ if (originalMobile !== newMobile) {
+ userPayload.mobile = newMobile;
+ }
+
+ const rolePayload = {
+ id: userPayload.id,
+ role_id: formValue.role,
+ };
+ const permissionPayload = {
+ id: userPayload.id,
+ cpermissions: formValue.permission,
+ };
+ this.store.dispatch(
+ otpActions.updateUserRole({ payload: rolePayload, authToken: this.userToken() })
+ );
+ this.store.dispatch(
+ otpActions.updateUserPermission({ payload: permissionPayload, authToken: this.userToken() })
+ );
+ }
+ } else {
+ // Add new user
+ const newUser: UserData = {
+ userId: (this.userData.length + 1).toString().padStart(3, '0'),
+ name: formValue.name,
+ email: formValue.email,
+ mobileNumber: formValue.mobileNumber || '',
+ role: roleName,
+ permissions: this.getDefaultPermissions(roleName),
+ };
+ const payload = {
+ user: {
+ name: newUser.name,
+ email: newUser.email,
+ mobile: newUser.mobileNumber,
+ },
+ role_id: formValue.role,
+ };
+
+ this.store.dispatch(otpActions.addUser({ payload, authToken: this.userToken() }));
+ }
+
+ this.closeDialog();
+ }
+ }
+
+ public getRoleById(roleId: number): Role | undefined {
+ return this.roles.find((role) => role.id === roleId);
+ }
+
+ public getVisiblePermissions(role: any): any[] {
+ if (!role || !role.c_permissions || role.c_permissions.length === 0) {
+ return [];
+ }
+ const isExpanded = this.expandedRoles[role.id] || false;
+ return isExpanded ? role.c_permissions : role.c_permissions.slice(0, 3);
+ }
+
+ public getDefaultPermissions(role: string): string[] {
+ switch (role) {
+ case 'Admin':
+ return ['Full Access', 'User Management', 'System Settings', 'Reports'];
+ case 'Manager':
+ return ['User Management', 'Reports', 'View Settings'];
+ case 'User':
+ return ['Read Only', 'View Reports'];
+ default:
+ return ['Read Only'];
+ }
+ }
+
+ public addRole(): void {
+ this.isEditRole = true;
+ this.isEditPermission = false;
+ this.currentEditingUser = null;
+ this.addRoleForm.reset();
+ this.openDialog();
+ }
+
+ public saveAddRole(): void {
+ if (!this.addRoleForm.valid) return;
+ const formValue = this.addRoleForm.value;
+ if (this.isEditRole && this.currentEditingUser) {
+ this.store.dispatch(
+ otpActions.updateRole({
+ payload: {
+ id: (this.currentEditingUser as any).id,
+ name: formValue.roleName,
+ cpermissions: formValue.permission,
+ },
+ authToken: this.userToken(),
+ })
+ );
+ } else {
+ this.store.dispatch(
+ otpActions.createRole({
+ name: formValue.roleName,
+ permissions: this.getPermissionNamesByIds(formValue.permission),
+ authToken: this.userToken(),
+ })
+ );
+ }
+ this.addRoleForm.reset();
+ this.closeDialog();
+ }
+
+ public saveAddPermissionTab(): void {
+ if (!this.addPermissionTabForm.valid) return;
+ const formValue = this.addPermissionTabForm.value;
+ if (this.isEditPermission && this.currentEditingPermission) {
+ this.store.dispatch(
+ otpActions.updatePermission({
+ payload: { id: (this.currentEditingPermission as any).id, name: formValue.permission },
+ authToken: this.userToken(),
+ })
+ );
+ } else {
+ this.store.dispatch(
+ otpActions.createPermission({ name: formValue.permission, authToken: this.userToken() })
+ );
+ }
+ this.addPermissionTabForm.reset();
+ this.closeDialog();
+ }
+ public onPermissionCheckboxChange(formName: 'addRoleForm' | 'addUserForm', permId: number, event: Event): void {
+ const checked = (event.target as HTMLInputElement).checked;
+ const form = this[formName];
+ const current: number[] = form.get('permission')?.value ?? [];
+ const updated = checked ? [...current, permId] : current.filter((id) => id !== permId);
+ form.get('permission')?.setValue(updated);
+ form.get('permission')?.markAsTouched();
+ }
+
+ public getCompanyUsers(search?: string): void {
+ const pageSize = this.currentPageSize;
+ const pageIndex = this.currentPageIndex;
+ const searchTerm = search?.trim() || undefined;
+ this.store.dispatch(
+ otpActions.getCompanyUsers({
+ authToken: this.userToken(),
+ itemsPerPage: pageSize,
+ pageNo: pageIndex,
+ search: searchTerm,
+ exclude_role_ids: this.exclude_role_ids(),
+ include_role_ids: this.include_role_ids(),
+ })
+ );
+ }
+
+ public onUsersPageChange(event: PageEvent): void {
+ this.currentPageIndex = event.pageIndex;
+ this.currentPageSize = event.pageSize;
+ const searchTerm = this.searchTerm?.trim() || undefined;
+ // API expects 1-based page number
+ this.store.dispatch(
+ otpActions.getCompanyUsers({
+ authToken: this.userToken(),
+ itemsPerPage: event.pageSize,
+ pageNo: event.pageIndex,
+ search: searchTerm,
+ })
+ );
+ }
+ public getRoles(): void {
+ this.store.dispatch(otpActions.getRoles({ authToken: this.userToken(), itemsPerPage: 1000 }));
+ }
+
+ public onRolesPageChange(event: PageEvent): void {
+ this.store.dispatch(otpActions.getRoles({ authToken: this.userToken(), itemsPerPage: event.pageSize }));
+ }
+ public getPermissions(): void {
+ const pageSize = 1000;
+ this.store.dispatch(otpActions.getPermissions({ authToken: this.userToken(), itemsPerPage: pageSize }));
+ }
+
+ public refreshFormData(): void {
+ setTimeout(() => {
+ this.filteredPermissionsData = [...this.permissions];
+ this.filteredRolesData = [...this.roles];
+ this.cdr.markForCheck();
+ }, 100);
+ }
+
+ // Role management methods
+ public applyRoleFilter(): void {
+ const q = this.roleSearchTerm.toLowerCase().trim();
+ this.filteredRolesData = q
+ ? this.roles.filter(
+ (role) =>
+ role.name.toLowerCase().includes(q) ||
+ role.c_permissions?.some((p: any) => p.name.toLowerCase().includes(q))
+ )
+ : [...this.roles];
+ }
+
+ public editRole(role: any, index: number): void {
+ this.currentEditingUser = role;
+ this.isEditRole = true;
+ this.isEditPermission = false;
+ this.isEditUser = false;
+
+ const permissionIds = role.c_permissions ? role.c_permissions.map((p: any) => p.id) : [];
+ const patchFn = () => {
+ this.addRoleForm.patchValue({
+ roleName: role.name,
+ description: `Description for ${role.name} role`,
+ permission: permissionIds,
+ });
+ };
+
+ this.openDialog();
+ if (this.permissions?.length > 0) {
+ patchFn();
+ } else {
+ setTimeout(patchFn, 500);
+ }
+ }
+
+ // Permission management methods
+ public applyPermissionFilter(): void {
+ const q = this.permissionSearchTerm.toLowerCase().trim();
+ this.filteredPermissionsData = q
+ ? this.permissions.filter((p) => p.name.toLowerCase().includes(q))
+ : [...this.permissions];
+ }
+
+ public openAddPermissionDialog(): void {
+ this.isEditPermission = true;
+ this.isEditRole = false;
+ this.currentEditingPermission = null;
+ this.addPermissionTabForm.reset();
+ this.openDialog();
+ }
+
+ public editPermission(permission: any, index: number): void {
+ this.currentEditingPermission = permission;
+ this.isEditPermission = true;
+ this.isEditRole = false;
+
+ this.addPermissionTabForm.patchValue({
+ permission: permission.name,
+ description: `Description for ${permission.name} permission`,
+ });
+
+ this.openDialog();
+ }
+
+ // Dialog helper methods
+ public getDialogTitle(): string {
+ if (this.isEditPermission) return this.currentEditingPermission ? 'Edit Permission' : 'Add New Permission';
+ if (this.isEditRole) return this.currentEditingUser ? 'Edit Role' : 'Add New Role';
+ return this.isEditUser ? 'Edit member' : 'Add New member';
+ }
+
+ public getSaveAction(): void {
+ if (this.isEditPermission) this.saveAddPermissionTab();
+ else if (this.isEditRole) this.saveAddRole();
+ else this.saveUser();
+ }
+
+ public getFormInvalid(): boolean {
+ if (this.isEditPermission) return this.addPermissionTabForm.invalid;
+ if (this.isEditRole) return this.addRoleForm.invalid;
+ return this.addUserForm.invalid;
+ }
+
+ public getSaveButtonText(): string {
+ if (this.isEditPermission) return this.currentEditingPermission ? 'Update Permission' : 'Add Permission';
+ if (this.isEditRole) return this.currentEditingUser ? 'Update Role' : 'Add Role';
+ return this.isEditUser ? 'Update member' : 'Add member';
+ }
+
+ public getAvailableAdditionalPermissions(): any[] {
+ if (!this.currentEditingUser) {
+ return [];
+ }
+ const userRole = this.roles.find((role) => role.name === this.currentEditingUser!.role);
+ const rolePermissionNames: string[] = userRole?.c_permissions?.map((p: any) => p.name) ?? [];
+ return this.permissions.filter((p) => !rolePermissionNames.includes(p.name));
+ }
+}
diff --git a/apps/36-blocks-widget/src/app/otp/user-profile/user-profile.component.html b/apps/36-blocks-widget/src/app/otp/user-profile/user-profile.component.html
new file mode 100644
index 00000000..a57c6afb
--- /dev/null
+++ b/apps/36-blocks-widget/src/app/otp/user-profile/user-profile.component.html
@@ -0,0 +1,353 @@
+
+
+
+
+
+
+
+
+
+ {{ previousName || 'U' | slice: 0:2 | uppercase }}
+
+
+
+ {{ previousName || 'User' }}
+
+
+ {{ clientForm.get('email')?.value }}
+
+
+
+
+
+
+ {{ (userDetails$ | async)?.c_companies?.length || 0 }} Organizations
+
+
+
+
+
+ @if (!isEditing) {
+
+
+
+
+
+
+
+
Full Name
+
+ {{ previousName || '—' }}
+
+
+
+
+
+
+
+
+
+
Mobile
+
+ {{
+ clientForm.get('mobile')?.value === '--Not Provided--'
+ ? 'Not provided'
+ : clientForm.get('mobile')?.value || 'Not provided'
+ }}
+
+
+
+
+
+
+
+
+
+
Email Address
+
+ {{ clientForm.get('email')?.value }}
+
+
+
+
+ }
+
+
+
+
+
+
+ @if ((userDetails$ | async)?.c_companies?.length) {
+
+
+ Organization
+ Actions
+
+ @for (org of (userDetails$ | async)?.c_companies; track org.id) {
+
+
+
+ {{ org.name | slice: 0:2 | uppercase }}
+
+
+
+ {{ org.name }}
+
+ @if (companyDetails?.currentCompany?.company_uname === org.company_uname) {
+
+ ● Current
+
+ }
+
+
+ @if (companyDetails?.currentCompany?.company_uname !== org.company_uname) {
+
↗ Leave
+ }
+
+ }
+
+ } @else {
+
+ Nothing here — there are no companies to show
+
+ }
+
+
+
+@if (isEditing) {
+
+
+
+
+
+
+
+
+
+ Full Name *
+
+
+ @if (clientForm.get('name')?.touched && clientForm.get('name')?.hasError('required')) {
+
Name is required.
+ } @else if (clientForm.get('name')?.touched && clientForm.get('name')?.hasError('pattern')) {
+
Invalid name format.
+ }
+
+
+ Mobile
+
+
+
+ Email Address
+
+
+
+
+
+
+
+
+} @if (confirmDialogCompanyId()) {
+
+}
+
diff --git a/apps/proxy-auth/src/app/otp/user-profile/user-profile.component.scss b/apps/36-blocks-widget/src/app/otp/user-profile/user-profile.component.scss
similarity index 61%
rename from apps/proxy-auth/src/app/otp/user-profile/user-profile.component.scss
rename to apps/36-blocks-widget/src/app/otp/user-profile/user-profile.component.scss
index 6d547b5f..e852dfe0 100644
--- a/apps/proxy-auth/src/app/otp/user-profile/user-profile.component.scss
+++ b/apps/36-blocks-widget/src/app/otp/user-profile/user-profile.component.scss
@@ -1,15 +1,7 @@
-// ─────────────────────────────────────────────────────────────────────────
-// user-profile.component.scss
-//
-// ViewEncapsulation.None is set on this component, so these styles are
-// injected as global CSS. Everything is scoped under .container to prevent
-// leaking into other components. No ::ng-deep needed anywhere.
-// ─────────────────────────────────────────────────────────────────────────
-
-// ── SHARED STRUCTURE ──────────────────────────────────────────────────────
-
+// Styling handled by Tailwind utility classes in the template.
+/*
.container {
- background: #ffffff;
+ background: var(--color-common-bg);
padding: 15px 60px;
text-align: left;
position: relative;
@@ -108,9 +100,8 @@
justify-content: center;
font-size: 20px;
font-weight: 700;
- color: #ffffff;
+ color: var(--color-common-white);
letter-spacing: -0.5px;
- // box-shadow: 0 4px 14px rgba(59,130,246,0.3);
}
.avatar-status {
@@ -120,7 +111,7 @@
width: 13px;
height: 13px;
border-radius: 50%;
- background: #22c55e;
+ background: var(--color-common-green);
}
.profile-name {
@@ -280,38 +271,6 @@
border-radius: 20px;
}
- .org-table {
- width: 100%;
-
- th.mat-header-cell {
- font-size: 10px;
- font-weight: 700;
- text-transform: uppercase;
- letter-spacing: 0.8px;
- padding: 10px 24px;
- }
-
- td.mat-cell {
- padding: 10px 24px;
- }
- .action-column {
- text-align: right;
- width: 120px;
- }
- .action-cell {
- text-align: right;
- .actions {
- opacity: 0;
- transition: opacity 0.2s ease;
- }
- }
- }
-
- tr.mat-mdc-row:hover .action-cell .actions,
- tr.mat-row:hover .action-cell .actions {
- opacity: 1;
- }
-
.org-cell-inner {
display: flex;
align-items: center;
@@ -375,18 +334,17 @@
// LIGHT THEME
// ══════════════════════════════════════════════
&.light-theme {
- color: #374151;
- background-color: white;
+ color: var(--color-common-text);
+ background-color: var(--color-common-bg);
.profile-card,
.org-card {
- background: #ffffff;
- border: 1px solid #e5e7eb;
- // box-shadow: 0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04);
+ background: var(--color-common-bg);
+ border: 1px solid var(--color-common-border);
}
.card-header {
- background: #f9fafb;
- border-bottom: 1px solid #e5e7eb;
+ background: var(--color-common-app-bg);
+ border-bottom: 1px solid var(--color-common-border);
}
.card-icon.blue {
background: #eff6ff;
@@ -418,11 +376,11 @@
.profile-banner {
background: linear-gradient(135deg, rgba(37, 99, 235, 0.04) 0%, transparent 60%);
- border-bottom: 1px solid #e5e7eb;
+ border-bottom: 1px solid var(--color-common-border);
}
.avatar-status {
- border: 2.5px solid #ffffff;
+ border: 2.5px solid var(--color-common-white);
}
.profile-name {
color: #111827;
@@ -437,10 +395,10 @@
}
.view-row {
- border-right-color: #e5e7eb;
- border-bottom-color: #e5e7eb;
+ border-right-color: var(--color-common-border);
+ border-bottom-color: var(--color-common-border);
&:hover {
- background: #f9fafb;
+ background: var(--color-common-app-bg);
}
}
.view-field-icon {
@@ -464,11 +422,11 @@
}
.btn-cancel {
- color: #374151 !important;
- border-color: #d1d5db !important;
- background: #ffffff !important;
+ color: var(--color-common-text) !important;
+ border-color: var(--color-common-border) !important;
+ background: var(--color-common-bg) !important;
&:hover {
- border-color: #9ca3af !important;
+ border-color: var(--color-common-grey) !important;
}
}
.btn-save {
@@ -479,74 +437,10 @@
}
}
- // Material form field — light
- .mat-mdc-form-field {
- .mat-mdc-text-field-wrapper {
- background-color: #ffffff;
- }
- .mat-mdc-input-element {
- color: #111827 !important;
- }
- .mat-mdc-floating-label {
- color: #6b7280 !important;
- }
- .mdc-floating-label {
- color: #6b7280 !important;
- }
- mat-label {
- color: #6b7280 !important;
- }
- }
- .mat-mdc-form-field-flex {
- .mdc-notched-outline__leading,
- .mdc-notched-outline__notch,
- .mdc-notched-outline__trailing {
- border-color: #d1d5db !important;
- }
- }
- .mdc-text-field--outlined .mdc-notched-outline__leading,
- .mdc-text-field--outlined .mdc-notched-outline__notch,
- .mdc-text-field--outlined .mdc-notched-outline__trailing {
- border-color: #d1d5db !important;
- }
- .mat-form-field-appearance-outline {
- .mat-form-field-outline {
- color: #d1d5db !important;
- }
- &.mat-form-field-disabled .mat-form-field-outline {
- background-color: #f9fafb !important;
- }
- }
- .mat-form-field-appearance-outline .mat-form-field-outline-thick {
- color: #2563eb !important;
- }
- .mat-form-field-appearance-outline.mat-form-field-invalid .mat-form-field-outline-thick {
- color: red !important;
- }
-
- // Org table — light
- .org-table {
- background: transparent;
- // box-shadow: none;
- th.mat-header-cell {
- background: #f9fafb;
- color: #9ca3af;
- border-bottom: 1px solid #e5e7eb;
- }
- td.mat-cell {
- color: #374151;
- border-bottom: 1px solid #f3f4f6;
- }
- }
- tr.mat-mdc-row:hover,
- tr.mat-row:hover {
- background: #f8fafd !important;
- }
-
.org-avatar-sm {
- background: #f3f4f6;
- border: 1px solid #e5e7eb;
- color: #9ca3af;
+ background: var(--color-common-bg-light);
+ border: 1px solid var(--color-common-border);
+ color: var(--color-common-grey);
&.current {
background: #eff6ff;
border-color: #dbeafe;
@@ -554,7 +448,7 @@
}
}
.org-name {
- color: #374151;
+ color: var(--color-common-text);
}
.current-org {
color: #111827 !important;
@@ -565,26 +459,26 @@
border: 1px solid #a7f3d0;
}
.btn-leave {
- background-color: #fef2f2 !important;
- color: #dc2626 !important;
- border: 1px solid #fecaca !important;
+ background-color: rgba(220, 38, 38, 0.06) !important;
+ color: var(--proxy-error-40) !important;
+ border: 1px solid rgba(220, 38, 38, 0.2) !important;
&:hover {
- background-color: #fee2e2 !important;
+ background-color: rgba(220, 38, 38, 0.12) !important;
}
}
.org-count-badge {
- background: #f3f4f6;
- color: #6b7280;
- border: 1px solid #e5e7eb;
+ background: var(--color-common-bg-light);
+ color: var(--color-common-grey);
+ border: 1px solid var(--color-common-border);
}
.no-data {
- color: #9ca3af;
+ color: var(--color-common-grey);
}
.success-message {
- color: #059669;
+ color: var(--color-common-green);
}
.error-message {
- color: #dc2626;
+ color: var(--proxy-error-40);
}
}
@@ -593,7 +487,6 @@
// No ::ng-deep needed — ViewEncapsulation.None
// ══════════════════════════════════════════════
&.dark-theme {
- // background: transparent;
color: #e5e7eb;
h2,
@@ -603,9 +496,7 @@
.profile-card,
.org-card {
- // background: #2C2C2E !important;
border: 1px solid rgba(255, 255, 255, 0.08) !important;
- // box-shadow: 0 2px 12px rgba(0,0,0,0.3), 0 1px 0 rgba(255,255,255,0.04) inset !important;
}
.card-header {
@@ -699,142 +590,11 @@
.btn-save {
background-color: #4a9eff !important;
color: #ffffff !important;
- // box-shadow: 0 2px 12px rgba(74,158,255,0.3) !important;
&:hover {
background-color: #5aaeff !important;
}
}
- // Material form field — dark (no ::ng-deep needed with ViewEncapsulation.None)
- .mat-mdc-form-field {
- .mat-mdc-text-field-wrapper {
- background-color: transparent !important;
- }
- .mat-mdc-input-element {
- color: #e0e0e0 !important;
- }
- .mat-mdc-floating-label {
- color: #a0a0a0 !important;
- }
- .mdc-floating-label {
- color: #a0a0a0 !important;
- }
- mat-label {
- color: #a0a0a0 !important;
- }
-
- // hint — "Contact support to update"
- .mat-mdc-form-field-hint {
- color: #9ca3af !important;
- }
- .mat-mdc-form-field-hint-wrapper {
- color: #9ca3af !important;
- }
- .mat-mdc-form-field-subscript-wrapper {
- color: #9ca3af !important;
- }
- .mat-hint {
- color: #9ca3af !important;
- }
-
- // error text
- .mat-mdc-form-field-error {
- color: #ff6b6b !important;
- }
- .mat-error {
- color: #ff6b6b !important;
- }
-
- // input placeholder
- .mat-mdc-input-element::placeholder {
- color: #5a5a6a !important;
- }
- input::placeholder {
- color: #5a5a6a !important;
- }
- }
-
- // hint & error also targeted at field level (Material renders them outside wrapper)
- .mat-mdc-form-field-hint {
- color: #9ca3af !important;
- }
- .mat-mdc-form-field-subscript-wrapper {
- color: #9ca3af !important;
- }
- .mat-mdc-form-field-bottom-align {
- color: #9ca3af !important;
- }
- .mat-hint {
- color: #9ca3af !important;
- }
- .mat-mdc-form-field-error {
- color: #ff6b6b !important;
- }
- .mat-error {
- color: #ff6b6b !important;
- }
-
- .mat-mdc-form-field-flex .mdc-floating-label {
- color: #a0a0a0 !important;
- }
- .mat-mdc-form-field .mat-mdc-floating-label {
- color: #a0a0a0 !important;
- }
- .mdc-text-field--outlined .mdc-floating-label {
- color: #a0a0a0 !important;
- }
- .mat-form-field-label {
- color: #ffffff !important;
- }
-
- .mat-mdc-form-field-flex {
- .mdc-notched-outline__leading,
- .mdc-notched-outline__notch,
- .mdc-notched-outline__trailing {
- border-color: rgba(255, 255, 255, 0.4) !important;
- }
- }
- .mdc-text-field--outlined .mdc-notched-outline__leading,
- .mdc-text-field--outlined .mdc-notched-outline__notch,
- .mdc-text-field--outlined .mdc-notched-outline__trailing {
- border-color: rgba(255, 255, 255, 0.4) !important;
- }
- .mat-form-field-appearance-outline {
- .mat-form-field-outline {
- color: rgba(255, 255, 255, 0.4) !important;
- }
- &.mat-form-field-disabled .mat-form-field-outline {
- background-color: #fafafa26 !important;
- }
- }
- .mat-form-field-appearance-outline .mat-form-field-outline-thick {
- color: #ffffff !important;
- }
- .mat-form-field-appearance-outline.mat-form-field-invalid .mat-form-field-outline-thick {
- color: red !important;
- }
- .input-field {
- color: #e0e0e0 !important;
- }
-
- // Org table — dark
- .org-table {
- background-color: transparent !important;
- th.mat-header-cell {
- background-color: rgba(0, 0, 0, 0.22) !important;
- color: #c5c5ca !important;
- border-bottom: 1px solid rgba(255, 255, 255, 0.12) !important;
- }
- td.mat-cell {
- color: #e5e7eb !important;
- border-bottom: 1px solid rgba(255, 255, 255, 0.08) !important;
- }
- }
- tr.mat-mdc-row:hover,
- tr.mat-row:hover {
- background-color: rgba(255, 255, 255, 0.07) !important;
- }
-
.org-avatar-sm {
background: rgba(255, 255, 255, 0.1) !important;
border: 1px solid rgba(255, 255, 255, 0.18) !important;
@@ -876,10 +636,10 @@
color: #c5c5ca !important;
}
.success-message {
- color: #4dd96a !important;
+ color: var(--color-common-green) !important;
}
.error-message {
- color: #ff6b63 !important;
+ color: var(--proxy-error-80) !important;
}
}
@@ -903,3 +663,4 @@
}
}
}
+*/
diff --git a/apps/proxy-auth/src/app/otp/user-profile/user-profile.component.ts b/apps/36-blocks-widget/src/app/otp/user-profile/user-profile.component.ts
similarity index 50%
rename from apps/proxy-auth/src/app/otp/user-profile/user-profile.component.ts
rename to apps/36-blocks-widget/src/app/otp/user-profile/user-profile.component.ts
index 5f0e4f68..d74f29df 100644
--- a/apps/proxy-auth/src/app/otp/user-profile/user-profile.component.ts
+++ b/apps/36-blocks-widget/src/app/otp/user-profile/user-profile.component.ts
@@ -1,10 +1,30 @@
-import { NgStyle } from '@angular/common';
-import { Component, Input, OnInit, SimpleChanges, ViewEncapsulation } from '@angular/core';
+import { CommonModule, NgStyle } from '@angular/common';
+import { ReactiveFormsModule } from '@angular/forms';
+import {
+ AfterViewInit,
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ ElementRef,
+ Input,
+ OnDestroy,
+ OnInit,
+ ViewChild,
+ ViewEncapsulation,
+ effect,
+ inject,
+ input,
+ signal,
+} from '@angular/core';
+import { WidgetPortalRef, WidgetPortalService } from '../service/widget-portal.service';
+import { ToastService } from '../service/toast.service';
+import { ToastComponent } from '../service/toast.component';
+import { ConfirmDialogComponent } from '../ui/confirm-dialog.component';
import { FormControl, FormGroup, Validators } from '@angular/forms';
-import { BehaviorSubject, distinctUntilChanged, map, Observable, of, takeUntil, take, filter } from 'rxjs';
+import { BehaviorSubject, distinctUntilChanged, map, Observable, takeUntil, take, filter } from 'rxjs';
import { IAppState } from '../store/app.state';
import { select, Store } from '@ngrx/store';
-import { getUserDetails, leaveCompany } from '../store/actions/otp.action';
+import { getUserDetails, leaveCompany, updateUser } from '../store/actions/otp.action';
import {
error,
getUserProfileData,
@@ -13,24 +33,28 @@ import {
leaveCompanySuccess,
} from '../store/selectors';
import { BaseComponent } from '@proxy/ui/base-component';
-import { isEqual } from 'lodash';
-import { Overlay } from '@angular/cdk/overlay';
-import { MatDialog } from '@angular/material/dialog';
-import { MatSnackBar } from '@angular/material/snack-bar';
-import { ConfirmationDialogComponent } from './user-dialog/user-dialog.component';
-import { updateUser } from '../store/actions/otp.action';
+import { isEqual } from 'lodash-es';
import { UPDATE_REGEX } from '@proxy/regex';
+import { WidgetTheme } from '@proxy/constant';
+import { WidgetThemeService } from '../service/widget-theme.service';
@Component({
- selector: 'proxy-user-profile',
+ selector: 'user-profile',
+ imports: [CommonModule, ReactiveFormsModule, ToastComponent, ConfirmDialogComponent],
templateUrl: './user-profile.component.html',
styleUrls: ['./user-profile.component.scss'],
encapsulation: ViewEncapsulation.None,
+ changeDetection: ChangeDetectionStrategy.OnPush,
})
-export class UserProfileComponent extends BaseComponent implements OnInit {
- @Input() public authToken: string;
- @Input() public target: string;
- @Input() public showCard: boolean;
- @Input() public theme: string;
+export class UserProfileComponent extends BaseComponent implements OnInit, AfterViewInit, OnDestroy {
+ public authToken = input();
+ public target = input();
+ public showCard = input();
+ public theme = input();
+ protected readonly WidgetTheme = WidgetTheme;
+ private readonly themeService = inject(WidgetThemeService);
+ get isDark(): boolean {
+ return this.themeService.isDark(this.theme() as WidgetTheme);
+ }
@Input()
set css(type: NgStyle['ngStyle']) {
this.cssSubject$.next(type);
@@ -51,9 +75,9 @@ export class UserProfileComponent extends BaseComponent implements OnInit {
: type
)
);
- @Input() public successReturn: (arg: any) => any;
- @Input() public failureReturn: (arg: any) => any;
- @Input() public otherData: { [key: string]: any } = {};
+ public successReturn = input<(arg: any) => any>();
+ public failureReturn = input<(arg: any) => any>();
+ public otherData = input<{ [key: string]: any }>({});
public userDetails$: Observable;
public userInProcess$: Observable;
public deleteCompany$: Observable;
@@ -70,15 +94,25 @@ export class UserProfileComponent extends BaseComponent implements OnInit {
email: new FormControl({ value: '', disabled: true }),
});
- displayedColumns: string[] = ['companyName', 'action'];
public isEditing = false;
- constructor(
- private store: Store,
- public dialog: MatDialog,
- private snackBar: MatSnackBar,
- private overlay: Overlay
- ) {
+
+ private store = inject>(Store);
+ readonly toastService = inject(ToastService);
+ private readonly widgetPortal = inject(WidgetPortalService);
+ private readonly cdr = inject(ChangeDetectorRef);
+ readonly confirmDialogCompanyId = signal(null);
+
+ @ViewChild('editDialogPortal') private editDialogPortalEl?: ElementRef;
+ @ViewChild('confirmDialogPortal') private confirmDialogPortalEl?: ElementRef;
+ @ViewChild('toastPortal') private toastPortalEl?: ElementRef;
+
+ private editDialogRef: WidgetPortalRef | null = null;
+ private confirmDialogPortalRef: WidgetPortalRef | null = null;
+ private toastPortalRef: WidgetPortalRef | null = null;
+
+ constructor() {
super();
+ effect(() => this.themeService.setInputTheme(this.theme()));
this.userDetails$ = this.store.pipe(
select(getUserProfileData),
distinctUntilChanged(isEqual),
@@ -98,6 +132,19 @@ export class UserProfileComponent extends BaseComponent implements OnInit {
this.error$ = this.store.pipe(select(error), distinctUntilChanged(isEqual), takeUntil(this.destroy$));
}
+ ngAfterViewInit(): void {
+ if (this.toastPortalEl?.nativeElement) {
+ this.toastPortalRef = this.widgetPortal.attach(this.toastPortalEl.nativeElement);
+ }
+ }
+
+ ngOnDestroy(): void {
+ this.editDialogRef?.detach();
+ this.confirmDialogPortalRef?.detach();
+ this.toastPortalRef?.detach();
+ super.ngOnDestroy();
+ }
+
ngOnInit(): void {
this.userDetails$.pipe(takeUntil(this.destroy$)).subscribe((res) => {
if (res) {
@@ -117,32 +164,45 @@ export class UserProfileComponent extends BaseComponent implements OnInit {
this.store.dispatch(
getUserDetails({
- request: this.authToken,
+ request: this.authToken(),
})
);
}
openModal(companyId: number): void {
- const dialogRef = this.dialog.open(ConfirmationDialogComponent, {
- width: '400px',
- data: { companyId: companyId, authToken: this.authToken, theme: this.theme },
- panelClass: this.theme === 'dark' ? 'confirm-dialog-dark' : 'confirm-dialog-light',
- // Prevent CDK BlockScrollStrategy from applying left/top on when dialog opens
- scrollStrategy: this.overlay.scrollStrategies.noop(),
- });
+ this.confirmDialogCompanyId.set(companyId);
+ this.cdr.detectChanges();
+ if (this.confirmDialogPortalEl?.nativeElement) {
+ this.confirmDialogPortalRef = this.widgetPortal.attach(this.confirmDialogPortalEl.nativeElement);
+ }
+ }
- dialogRef.afterClosed().subscribe((result) => {
- if (result === 'confirmed') {
- this.store.dispatch(
- getUserDetails({
- request: this.authToken,
- })
- );
+ confirmLeave(): void {
+ this.confirmDialogPortalRef?.detach();
+ this.confirmDialogPortalRef = null;
+ const companyId = this.confirmDialogCompanyId();
+ this.confirmDialogCompanyId.set(null);
+ if (!companyId) return;
+ this.store.dispatch(leaveCompany({ companyId, authToken: this.authToken() }));
+ this.deleteCompany$.pipe(filter(Boolean), take(1)).subscribe((res) => {
+ if (res) {
+ window.parent.postMessage({ type: 'proxy', data: { event: 'userLeftCompany', companyId } }, '*');
+ this.store.dispatch(getUserDetails({ request: this.authToken() }));
}
});
}
+ public openEditDialog(): void {
+ this.isEditing = true;
+ this.cdr.detectChanges();
+ if (this.editDialogPortalEl?.nativeElement) {
+ this.editDialogRef = this.widgetPortal.attach(this.editDialogPortalEl.nativeElement);
+ }
+ }
+
public cancelEdit() {
+ this.editDialogRef?.detach();
+ this.editDialogRef = null;
this.isEditing = false;
this.clientForm.get('name').setValue(this.previousName);
}
@@ -151,6 +211,8 @@ export class UserProfileComponent extends BaseComponent implements OnInit {
const nameControl = this.clientForm.get('name');
const enteredName = nameControl?.value?.trim();
if (enteredName === this.previousName) {
+ this.editDialogRef?.detach();
+ this.editDialogRef = null;
this.isEditing = false;
return;
}
@@ -165,42 +227,27 @@ export class UserProfileComponent extends BaseComponent implements OnInit {
return;
}
- this.store.dispatch(updateUser({ name: enteredName, authToken: this.authToken }));
+ this.store.dispatch(updateUser({ name: enteredName, authToken: this.authToken() }));
this.update$.pipe(filter(Boolean), take(1)).subscribe((res) => {
if (res) {
+ this.editDialogRef?.detach();
+ this.editDialogRef = null;
this.isEditing = false;
this.previousName = enteredName;
- this.snackBar.open('Information successfully updated', '✕', {
- duration: 10000,
- horizontalPosition: 'center',
- verticalPosition: 'top',
- panelClass: ['success-snackbar'],
- });
+ this.toastService.success('Information successfully updated');
}
});
this.error$.pipe(filter(Boolean), take(1)).subscribe((err) => {
- if (err) {
- this.snackBar.open(err[0], '✕', {
- duration: 10000,
- horizontalPosition: 'center',
- verticalPosition: 'top',
- panelClass: ['error-snackbar'],
- });
- }
+ if (err?.[0]) this.toastService.error(err[0]);
});
window.parent.postMessage({ type: 'proxy', data: { event: 'userNameUpdated', enteredName: enteredName } }, '*');
}
public clear() {
- this.snackBar.open('Something went wrong', '✕', {
- duration: 3000,
- horizontalPosition: 'center',
- verticalPosition: 'top',
- panelClass: ['error-snackbar'],
- });
+ this.toastService.error('Something went wrong');
setTimeout(() => {
this.errorMessage = '';
}, 3000);
diff --git a/apps/36-blocks-widget/src/app/otp/widget/utility/model.ts b/apps/36-blocks-widget/src/app/otp/widget/utility/model.ts
new file mode 100644
index 00000000..fe72f6aa
--- /dev/null
+++ b/apps/36-blocks-widget/src/app/otp/widget/utility/model.ts
@@ -0,0 +1,17 @@
+// import { PublicScriptType } from '@proxy/constant';
+
+export enum WidgetVersion {
+ V1 = 'v1',
+ V2 = 'v2',
+}
+export enum InputFields {
+ TOP = 'top',
+ BOTTOM = 'bottom',
+}
+// export enum ViewMode {
+// OtpDialog = 'otp-dialog',
+// UserManagement = PublicScriptType.UserManagement,
+// Subscription = PublicScriptType.Subscription,
+// UserProfile = PublicScriptType.UserProfile,
+// OrganizationDetails = PublicScriptType.OrganizationDetails,
+// }
diff --git a/apps/36-blocks-widget/src/app/otp/widget/widget.component.html b/apps/36-blocks-widget/src/app/otp/widget/widget.component.html
new file mode 100644
index 00000000..8941b0c1
--- /dev/null
+++ b/apps/36-blocks-widget/src/app/otp/widget/widget.component.html
@@ -0,0 +1,156 @@
+@if (show()) {
+
+ @switch (viewMode()) { @case (PublicScriptType.UserManagement) {
+
+ } @case (PublicScriptType.Subscription) {
+
+ } @case (PublicScriptType.OrganizationDetails) {
+
+ } @case (PublicScriptType.UserProfile) {
+
+ } @case (PublicScriptType.Authorization) {
+
+ @if (isOtpLoading()) {
+
+ }
+
+
+ } }
+
+} @if (showRegistration() || showLogin() || showForgotPassword()) {
+
+
+
+ @if (isOtpLoading()) {
+
+ } @if (showForgotPassword()) {
+
+ }
+
+ @if (showRegistration()) {
+
+ } @if (showLogin()) {
+
+ } @if (showForgotPassword()) {
+
+ }
+
+
+
+}
+
+
diff --git a/apps/36-blocks-widget/src/app/otp/widget/widget.component.scss b/apps/36-blocks-widget/src/app/otp/widget/widget.component.scss
new file mode 100644
index 00000000..96a1a69b
--- /dev/null
+++ b/apps/36-blocks-widget/src/app/otp/widget/widget.component.scss
@@ -0,0 +1,39 @@
+@use 'intl-tel-input/build/css/intlTelInput.css';
+@use '../../../otp-global' as *;
+@use 'tailwindcss';
+@source not inline("group-[aria-current=page]:text-indigo-600");
+@custom-variant dark (&:where(.dark, .dark *));
+
+:host {
+ display: block !important;
+ height: inherit !important;
+ min-height: inherit !important;
+ max-height: inherit !important;
+ background: transparent !important;
+
+ // Prevent client-page CSS from bleeding into the widget.
+ // Shadow DOM already blocks inherited styles at the boundary,
+ // but these resets guard against any CSS that targets :host from outside.
+ all: initial;
+ display: block !important;
+ height: inherit !important;
+ min-height: inherit !important;
+ max-height: inherit !important;
+ background: transparent !important;
+
+ // Establish an independent stacking context so position:fixed children
+ // (dialogs, backdrops) are positioned relative to the viewport, not clipped
+ // by a client-side transform or will-change on an ancestor.
+ isolation: isolate;
+
+ // Ensure base typography is not inherited from the client page.
+ font-family: Inter, ui-sans-serif, system-ui, sans-serif;
+ font-size: 16px;
+ line-height: 1.5;
+ color: inherit;
+ box-sizing: border-box;
+}
+
+.authorization-container {
+ @apply h-full flex justify-center items-center;
+}
diff --git a/apps/proxy-auth/src/app/otp/send-otp/send-otp.component.ts b/apps/36-blocks-widget/src/app/otp/widget/widget.component.ts
similarity index 52%
rename from apps/proxy-auth/src/app/otp/send-otp/send-otp.component.ts
rename to apps/36-blocks-widget/src/app/otp/widget/widget.component.ts
index cb8b49dd..faaf15b3 100644
--- a/apps/proxy-auth/src/app/otp/send-otp/send-otp.component.ts
+++ b/apps/36-blocks-widget/src/app/otp/widget/widget.component.ts
@@ -1,13 +1,37 @@
import { OtpService } from './../service/otp.service';
-import { NgStyle } from '@angular/common';
-import { Component, Input, NgZone, OnDestroy, OnInit, Renderer2, ViewEncapsulation } from '@angular/core';
-import { META_TAG_ID } from '@proxy/constant';
+import { CommonModule } from '@angular/common';
+import { ProgressBarComponent } from '../ui/progress-bar.component';
+import { SendOtpCenterComponent } from '../component';
+import { RegisterComponent } from '../component/register/register.component';
+import { LoginComponent } from '../component/login/login.component';
+import { UserProfileComponent } from '../user-profile/user-profile.component';
+import { UserManagementComponent } from '../user-management/user-management.component';
+import { OrganizationDetailsComponent } from '../organization-details/organization-details.component';
+import {
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ ElementRef,
+ Input,
+ NgZone,
+ OnChanges,
+ OnDestroy,
+ OnInit,
+ Renderer2,
+ SimpleChanges,
+ ViewChild,
+ ViewEncapsulation,
+ computed,
+ effect,
+ inject,
+ signal,
+} from '@angular/core';
+import { toSignal } from '@angular/core/rxjs-interop';
+import { META_TAG_ID, WidgetTheme, PublicScriptType } from '@proxy/constant';
import { BaseComponent } from '@proxy/ui/base-component';
import { select, Store } from '@ngrx/store';
import { isEqual } from 'lodash-es';
-import { BehaviorSubject, Observable, of } from 'rxjs';
-import { distinctUntilChanged, filter, map, skip, take, takeUntil } from 'rxjs/operators';
-import { MatDialog } from '@angular/material/dialog';
+import { distinctUntilChanged, filter, skip, take, takeUntil } from 'rxjs/operators';
import { getSubscriptionPlans, getWidgetData, upgradeSubscription } from '../store/actions/otp.action';
import { IAppState } from '../store/app.state';
@@ -22,150 +46,174 @@ import {
} from '../store/selectors';
import { FeatureServiceIds } from '@proxy/models/features-model';
import { OtpWidgetService } from '../service/otp-widget.service';
+import { WidgetThemeService } from '../service/widget-theme.service';
import { OtpUtilityService } from '../service/otp-utility.service';
+import { SubscriptionRendererService } from '../service/subscription-renderer.service';
+import { ProxyAuthDomBuilderService } from '../service/proxy-auth-dom-builder.service';
import { HttpErrorResponse } from '@angular/common/http';
import { SubscriptionCenterComponent } from '../component/subscription-center/subscription-center.component';
-import { environment } from 'apps/proxy-auth/src/environments/environment';
-
-export enum Theme {
- LIGHT = 'light',
- DARK = 'dark',
- SYSTEM = 'system',
-}
-export enum SendOtpCenterVersion {
- V1 = 'v1',
- V2 = 'v2',
-}
-export enum InputFields {
- TOP = 'top',
- BOTTOM = 'bottom',
-}
-
+import { environment } from 'apps/36-blocks-widget/src/environments/environment';
+import { InputFields, WidgetVersion } from './utility/model';
+import { WidgetPortalRef, WidgetPortalService } from '../service/widget-portal.service';
@Component({
- selector: 'proxy-send-otp',
- templateUrl: './send-otp.component.html',
+ selector: 'proxy-auth-widget',
+ imports: [
+ CommonModule,
+ ProgressBarComponent,
+ SubscriptionCenterComponent,
+ SendOtpCenterComponent,
+ RegisterComponent,
+ LoginComponent,
+ UserProfileComponent,
+ UserManagementComponent,
+ OrganizationDetailsComponent,
+ ],
+ templateUrl: './widget.component.html',
encapsulation: ViewEncapsulation.ShadowDom,
- styleUrls: ['../../../styles.scss', './send-otp.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ styleUrls: ['../../../styles.scss', './widget.component.scss'],
})
-export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy {
+export class ProxyAuthWidgetComponent extends BaseComponent implements OnInit, OnChanges, OnDestroy {
@Input() public referenceId: string;
- @Input() public type: string;
@Input() public target: string;
- @Input() public authToken: string;
@Input() public showCompanyDetails: boolean;
@Input() public userToken: string;
- @Input() public isRolePermission: string;
- @Input() public isPreview: boolean;
- @Input() public isLogin: boolean;
+ @Input() public isRolePermission: boolean;
@Input() public loginRedirectUrl: string;
+ @Input() public authToken: string;
+ @Input() public type: string;
+ @Input() public isPreview: boolean = false;
+ @Input() public isLogin: boolean = false;
@Input() public theme: string;
- @Input() public version: string = SendOtpCenterVersion.V1;
+
+ private readonly _authToken$ = signal(undefined);
+ private readonly _type$ = signal(undefined);
+ private readonly themeService = inject(WidgetThemeService);
+ protected readonly WidgetTheme = WidgetTheme;
+ protected readonly PublicScriptType = PublicScriptType;
+
+ readonly viewMode = computed(() => {
+ const authToken = this._authToken$();
+ const type = this._type$();
+ if (authToken && type === PublicScriptType.UserManagement) {
+ return PublicScriptType.UserManagement;
+ }
+ if (authToken && type === PublicScriptType.OrganizationDetails) {
+ return PublicScriptType.OrganizationDetails;
+ }
+ if (authToken && type === PublicScriptType.UserProfile) {
+ return PublicScriptType.UserProfile;
+ }
+ // TODO: Uncomment when subscription is implemented
+ // if (type === PublicScriptType.Subscription) {
+ // return PublicScriptType.Subscription;
+ // }
+ return PublicScriptType.Authorization;
+ });
+
+ get isDarkTheme(): boolean {
+ return this.themeService.isDark(this.theme as WidgetTheme);
+ }
+
+ @Input() public version: string = WidgetVersion.V1;
@Input() public exclude_role_ids: any[] = [];
@Input() public include_role_ids: any[] = [];
@Input() public isHidden: boolean = false;
@Input() public input_fields: string = InputFields.TOP;
@Input() public show_social_login_icons: boolean = false;
@Input() public isRegisterFormOnly: boolean = false;
- set css(type: NgStyle['ngStyle']) {
- this.cssSubject$.next(type);
- }
- private readonly cssSubject$: NgStyle['ngStyle'] = new BehaviorSubject({
- position: 'absolute',
- 'margin-left': '50%',
- top: '10px',
- });
- readonly css$ = this.cssSubject$.pipe(
- map((type) =>
- !type || !Object.keys(type).length
- ? {
- position: 'absolute',
- 'margin-left': '50%',
- top: '10px',
- }
- : type
- )
- );
@Input() public successReturn: (arg: any) => any;
@Input() public failureReturn: (arg: any) => any;
@Input() public otherData: { [key: string]: any } = {};
- public show$: Observable = of(false);
- public selectGetOtpInProcess$: Observable;
- public selectWidgetData$: Observable;
- public selectResendOtpInProcess$: Observable;
- public selectVerifyOtpInProcess$: Observable;
- public selectWidgetTheme$: Observable;
- public animate: boolean = false;
+ @ViewChild('dialogPortal') private dialogPortalEl?: ElementRef;
+ private dialogPortalRef: WidgetPortalRef | null = null;
+ private readonly widgetPortal = inject(WidgetPortalService);
+
+ public readonly show = signal(false);
+ public readonly showRegistration = signal(false);
+ public readonly showForgotPassword = signal(false);
+ public readonly animate = signal(false);
public isCreateAccountTextAppended: boolean = false;
public otpWidgetData;
public loginWidgetData;
- public showRegistration = new BehaviorSubject(false);
public registrationViaLogin: boolean = true;
public prefillDetails: string;
- public cameFromLogin: boolean = false; // Track if user came from login
- public cameFromSendOtpCenter: boolean = false; // Track if user came from send-otp-center component
+ public cameFromLogin: boolean = false;
+ public cameFromSendOtpCenter: boolean = false;
public referenceElement: HTMLElement = null;
- public authReference: HTMLElement = null;
- public showCard: boolean = false;
public subscriptionPlans: any[] = [];
- public showLogin: BehaviorSubject = this.otpWidgetService.showlogin;
- public showSkeleton: boolean = false;
- public upgradeSubscriptionData: any;
+
+ private readonly cdr = inject(ChangeDetectorRef);
+
+ private readonly otpWidgetService = inject(OtpWidgetService);
+ private readonly store = inject>(Store);
+ private readonly ngZone = inject(NgZone);
+ private readonly renderer = inject(Renderer2);
+ private readonly otpUtilityService = inject(OtpUtilityService);
+ private readonly subscriptionRenderer = inject(SubscriptionRendererService);
+ private readonly domBuilder = inject(ProxyAuthDomBuilderService);
+ private readonly otpService = inject(OtpService);
+
+ readonly isOtpInProcess = toSignal(this.store.pipe(select(selectGetOtpInProcess), distinctUntilChanged(isEqual)), {
+ initialValue: false,
+ });
+ readonly isResendOtpInProcess = toSignal(
+ this.store.pipe(select(selectResendOtpInProcess), distinctUntilChanged(isEqual)),
+ { initialValue: false }
+ );
+ readonly isVerifyOtpInProcess = toSignal(
+ this.store.pipe(select(selectVerifyOtpInProcess), distinctUntilChanged(isEqual)),
+ { initialValue: false }
+ );
+ readonly isOtpLoading = computed(
+ () => this.isOtpInProcess() || this.isResendOtpInProcess() || this.isVerifyOtpInProcess()
+ );
+ readonly showLogin = toSignal(this.otpWidgetService.showlogin, { initialValue: false });
+
+ readonly widgetTheme = toSignal(this.store.pipe(select(selectWidgetTheme), distinctUntilChanged(isEqual)), {
+ initialValue: null,
+ });
+
+ private showSkeleton: boolean = false;
public dialogBorderRadius: string = null;
- private createAccountTextAppended: boolean = false; // Flag to track if create account text has been appended
+ private createAccountTextAppended: boolean = false;
private hcaptchaLoading: boolean = false;
private hcaptchaRenderQueue: Array<() => void> = [];
public isUserProxyContainer: boolean = true;
- constructor(
- private ngZone: NgZone,
- private store: Store,
- private renderer: Renderer2,
- private otpWidgetService: OtpWidgetService,
- private otpUtilityService: OtpUtilityService,
- private otpService: OtpService,
- private dialog: MatDialog
- ) {
+ constructor() {
super();
- this.selectGetOtpInProcess$ = this.store.pipe(
- select(selectGetOtpInProcess),
- distinctUntilChanged(isEqual),
- takeUntil(this.destroy$)
- );
- this.selectResendOtpInProcess$ = this.store.pipe(
- select(selectResendOtpInProcess),
- distinctUntilChanged(isEqual),
- takeUntil(this.destroy$)
- );
- this.selectVerifyOtpInProcess$ = this.store.pipe(
- select(selectVerifyOtpInProcess),
- distinctUntilChanged(isEqual),
- takeUntil(this.destroy$)
- );
- this.selectWidgetData$ = this.store.pipe(select(selectWidgetData), takeUntil(this.destroy$));
- this.selectWidgetTheme$ = this.store.pipe(select(selectWidgetTheme), takeUntil(this.destroy$));
+ effect(() => {
+ const dark = this.themeService.isDark$();
+ this.reapplyInjectedButtonTheme(dark);
+ });
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes['authToken']) this._authToken$.set(this.authToken);
+ if (changes['type']) this._type$.set(this.type);
+ if (changes['theme']) this.themeService.setInputTheme(this.theme);
}
ngOnInit() {
- const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
- prefersDark.addEventListener('change', (event) => {
- this.theme = event?.matches ? Theme.DARK : Theme.LIGHT;
- });
- if (!this.theme) {
- this.theme = prefersDark.matches ? Theme.DARK : Theme.LIGHT;
- }
- this.selectWidgetTheme$.pipe(filter(Boolean), takeUntil(this.destroy$)).subscribe((theme) => {
- if (theme?.ui_preferences?.theme !== Theme.SYSTEM) {
- this.theme = theme?.ui_preferences.theme || theme;
- }
- this.loginWidgetData = theme?.registerState;
- this.version = theme?.ui_preferences?.version || 'v1';
- this.input_fields = theme?.ui_preferences?.input_fields || 'top';
- this.show_social_login_icons = theme?.ui_preferences?.icons || false;
- this.isCreateAccountTextAppended = theme?.ui_preferences?.create_account_link || false;
- this.dialogBorderRadius = this.getBorderRadiusCssValue(theme?.ui_preferences?.border_radius);
- });
- if (this.type === 'subscription') {
+ this._authToken$.set(this.authToken);
+ this._type$.set(this.type);
+ this.themeService.setInputTheme(this.theme);
+ this.store
+ .pipe(select(selectWidgetTheme), filter(Boolean), takeUntil(this.destroy$))
+ .subscribe((theme: any) => {
+ if (theme?.ui_preferences?.theme !== WidgetTheme.System) {
+ this.themeService.setThemeOverride(theme?.ui_preferences?.theme || theme);
+ }
+ this.loginWidgetData = theme?.registerState;
+ this.version = theme?.ui_preferences?.version || 'v1';
+ this.input_fields = theme?.ui_preferences?.input_fields || 'top';
+ this.show_social_login_icons = theme?.ui_preferences?.icons || false;
+ this.isCreateAccountTextAppended = theme?.ui_preferences?.create_account_link || false;
+ this.dialogBorderRadius = this.getBorderRadiusCssValue(theme?.ui_preferences?.border_radius);
+ });
+ if (this.type === PublicScriptType.Subscription) {
// Load subscription plans first
this.store.dispatch(getSubscriptionPlans({ referenceId: this.referenceId, authToken: this.authToken }));
this.store.pipe(select(subscriptionPlansData), takeUntil(this.destroy$)).subscribe((subscriptionPlans) => {
@@ -173,7 +221,7 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
this.subscriptionPlans = subscriptionPlans.data;
}
if (this.isPreview) {
- this.show$ = of(true);
+ this.show.set(true);
} else {
this.toggleSendOtp(true);
}
@@ -182,7 +230,7 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
// Fallback timeout in case subscription plans don't load
setTimeout(() => {
if (this.isPreview) {
- this.show$ = of(true);
+ this.show.set(true);
} else if (!this.subscriptionPlans || this.subscriptionPlans.length === 0) {
this.toggleSendOtp(true);
}
@@ -195,34 +243,46 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
}
}
this.loadExternalFonts();
- this.store.dispatch(
- getWidgetData({
- referenceId: this.referenceId,
- payload: this.otherData,
- })
- );
- this.selectWidgetData$.pipe(filter(Boolean), takeUntil(this.destroy$)).subscribe((widgetData) => {
- this.otpWidgetData = widgetData?.find((widget) => widget?.service_id === FeatureServiceIds.Msg91OtpService);
- if (this.otpWidgetData) {
- this.otpWidgetService.setWidgetConfig(
- this.otpWidgetData?.widget_id,
- this.otpWidgetData?.token_auth,
- this.otpWidgetData?.state
+ if (!this.authToken) {
+ if (this.referenceId) {
+ this.store.dispatch(
+ getWidgetData({
+ referenceId: this.referenceId,
+ payload: this.otherData,
+ })
);
- this.otpWidgetService.loadScript();
+ } else {
+ console.error('Reference Id is undefined ! Please provide referenceId in the widget configuration.');
}
- if (!this.loginWidgetData) {
- this.loginWidgetData = widgetData?.find(
- (widget) => widget?.service_id === FeatureServiceIds.PasswordAuthentication
+ }
+ this.store
+ .pipe(select(selectWidgetData), filter(Boolean), takeUntil(this.destroy$))
+ .subscribe((widgetData: any[]) => {
+ this.otpWidgetData = widgetData?.find(
+ (widget) => widget?.service_id === FeatureServiceIds.Msg91OtpService
);
- }
- });
+ if (this.otpWidgetData) {
+ this.otpWidgetService.setWidgetConfig(
+ this.otpWidgetData?.widget_id,
+ this.otpWidgetData?.token_auth,
+ this.otpWidgetData?.state
+ );
+ this.otpWidgetService.loadScript();
+ }
+ if (!this.loginWidgetData) {
+ this.loginWidgetData = widgetData?.find(
+ (widget) => widget?.service_id === FeatureServiceIds.PasswordAuthentication
+ );
+ }
+ });
this.otpWidgetService.otpWidgetToken.pipe(filter(Boolean), takeUntil(this.destroy$)).subscribe((token) => {
this.hitCallbackUrl(this.otpWidgetData.callbackUrl, { state: this.otpWidgetData?.state, code: token });
});
}
ngOnDestroy() {
+ this.dialogPortalRef?.detach();
+ this.dialogPortalRef = null;
if (this.referenceElement) {
this.clearSubscriptionPlans(this.referenceElement);
}
@@ -232,6 +292,22 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
super.ngOnDestroy();
}
+ public closeOverlayDialog(): void {
+ this.dialogPortalRef?.detach();
+ this.dialogPortalRef = null;
+ this.ngZone.run(() => {
+ this.showRegistration.set(false);
+ this.showForgotPassword.set(false);
+ this.otpWidgetService.openLogin(false);
+ this.otpWidgetService.closeForgotPassword();
+ if (this.referenceElement) {
+ this.show.set(false);
+ }
+ this.cameFromLogin = false;
+ this.cameFromSendOtpCenter = false;
+ });
+ }
+
private loadExternalFonts() {
const node = document.querySelector('proxy-auth')?.shadowRoot;
const styleElement = document.createElement('link');
@@ -249,36 +325,41 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
public toggleSendOtp(intial: boolean = false) {
this.referenceElement = document.getElementById(this.referenceId);
if (!this.referenceElement) {
- this.show$.pipe(take(1)).subscribe((res) => {
- this.ngZone.run(() => {
- if (res) {
- this.animate = true;
- this.setShowLogin(false);
- setTimeout(() => {
- this.show$ = of(!res);
- this.animate = false;
- }, 300);
- } else {
- this.show$ = of(!res);
- }
- });
+ this.ngZone.run(() => {
+ const current = this.show();
+ if (current) {
+ this.animate.set(true);
+ this.setShowLogin(false);
+ setTimeout(() => {
+ this.show.set(false);
+ this.animate.set(false);
+ }, 300);
+ } else {
+ this.show.set(true);
+ }
});
} else {
this.setShowLogin(false);
this.isUserProxyContainer = false;
- this.show$ = of(false);
- this.animate = false;
+ this.show.set(false);
+ this.animate.set(false);
this.createAccountTextAppended = false;
if (intial) {
- if (this.type === 'subscription') {
+ if (this.type === PublicScriptType.Subscription) {
if (!this.isPreview && this.referenceElement) {
this.appendSubscriptionButton(this.referenceElement);
}
} else {
this.showSkeleton = true;
- this.appendSkeletonLoader(this.referenceElement, 1);
+ this.domBuilder.appendSkeletonLoader(this.renderer, this.referenceElement);
this.addButtonsToReferenceElement(this.referenceElement);
+ setTimeout(() => {
+ if (this.showSkeleton) {
+ this.showSkeleton = false;
+ this.domBuilder.forceRemoveAllSkeletonLoaders(this.renderer, this.referenceElement);
+ }
+ }, 10000);
}
}
}
@@ -357,552 +438,22 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
});
}
- // Method to disable Angular subscription component
- public disableAngularSubscription(): void {
- this.type = 'custom-subscription';
- }
-
private createSubscriptionCenterHTML(): string {
- const plans = this.subscriptionPlans || [];
-
- if (plans.length === 0) {
- return `
-
-
-
- No subscription plans available
-
-
-
- `;
- }
-
- const plansHTML = plans.map((plan) => this.createPlanCardHTML(plan)).join('');
-
- const finalHTML = `
-
- `;
-
- return finalHTML;
- }
-
- private createPlanCardHTML(plan: any): string {
- // Map the hardcoded JSON structure to the expected format
- const isPopular = plan.plan_meta?.highlight_plan || false;
- const popularClass = isPopular ? 'popular' : '';
- const selectedClass = plan.isSelected ? 'selected' : '';
- const highlightedClass = isPopular ? 'highlighted' : '';
-
- const popularBadge = plan.plan_meta?.tag ? `${plan.plan_meta.tag}
` : '';
-
- // Extract price value and currency from "1000 USD"
- const priceMatch = plan.plan_price?.match(/(\d+)\s+(.+)/);
- const priceValue = priceMatch ? priceMatch[1] : '0';
- const currency = priceMatch ? priceMatch[2] : 'USD';
-
- const metricsHTML =
- plan.plan_meta?.metrics && plan.plan_meta.metrics.length > 0
- ? `
-
-
Included
-
- ${plan.plan_meta.metrics.map((metric) => `
${metric}
`).join('')}
-
-
- `
- : '';
-
- const featuresHTML =
- (plan.plan_meta?.features?.included && plan.plan_meta.features.included.length > 0) ||
- (plan.plan_meta?.features?.notIncluded && plan.plan_meta.features.notIncluded.length > 0)
- ? `
-
-
Features
-
- ${
- plan.plan_meta.features.included
- ? plan.plan_meta.features.included
- .map(
- (feature) => `
-
-
-
-
-
-
- ${feature}
-
- `
- )
- .join('')
- : ''
- }
- ${
- plan.plan_meta.features.notIncluded
- ? plan.plan_meta.features.notIncluded
- .map(
- (feature) => `
-
-
-
-
-
-
- ${feature}
-
- `
- )
- .join('')
- : ''
- }
-
-
- `
- : '';
-
- const extraFeaturesHTML =
- plan.plan_meta?.extra && plan.plan_meta.extra.length > 0
- ? `
-
-
Extra
-
- ${plan.plan_meta.extra
- .map(
- (extraFeature) => `
-
- `
- )
- .join('')}
-
-
- `
- : '';
-
- const isDisabled = !!plan.isSubscribed;
- const buttonHTML = `
-
- ${this.isLogin ? (plan.isSubscribed ? 'Your current plan' : 'Get ' + plan.plan_name) : 'Get Started'}
-
- `;
-
- return `
-
- ${popularBadge}
-
-
${plan.plan_name}
-
-
- ${priceValue}
- ${currency}
-
-
- ${buttonHTML}
-
-
- ${metricsHTML}
- ${featuresHTML}
- ${extraFeaturesHTML}
-
- `;
+ return this.subscriptionRenderer.buildContainerHTML(
+ this.subscriptionPlans || [],
+ this.themeService.isDark(),
+ this.isLogin
+ );
}
- /**
- * Create a plan card element
- */
-
- /**
- * Add CSS styles for subscription plans
- */
private addSubscriptionStyles(): void {
- // Check if styles are already added
- if (document.getElementById('subscription-styles')) {
- return;
- }
-
- const style = this.renderer.createElement('style');
- style.id = 'subscription-styles';
- style.textContent = `
- @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&display=swap');
-
- /* Bootstrap-like utility classes */
- .position-relative { position: relative !important; }
- .d-flex { display: flex !important; }
- .d-block { display: block !important; }
- .flex-row { flex-direction: row !important; }
- .flex-column { flex-direction: column !important; }
- .align-items-center { align-items: center !important; }
- .align-items-stretch { align-items: stretch !important; }
- .justify-content-start { justify-content: flex-start !important; }
- .justify-content-between { justify-content: space-between !important; }
- .w-100 { width: 100% !important; }
- .p-0 { padding: 0 !important; }
- .p-3 { padding: 1rem !important; }
- .pt-3, .py-3 { padding-top: 1rem !important; }
- .pb-3, .py-3 { padding-bottom: 1rem !important; }
- .m-0 { margin: 0 !important; }
- .mt-0, .my-0 { margin-top: 0 !important; }
- .mb-0, .my-0 { margin-bottom: 0 !important; }
- .mb-2 { margin-bottom: 0.5rem !important; }
- .mb-3 { margin-bottom: 1rem !important; }
- .mb-4 { margin-bottom: 1.5rem !important; }
- .my-3 { margin-top: 1rem !important; margin-bottom: 1rem !important; }
- .text-center { text-align: center !important; }
- .text-left { text-align: left !important; }
- .gap-1 { gap: 0.25rem !important; }
- .gap-2 { gap: 0.5rem !important; }
- .gap-3 { gap: 1rem !important; }
- .gap-4 { gap: 1.5rem !important; }
- .gap-5 { gap: 2rem !important; }
-
-
-
- /* Subscription Plans Styles */
-.subscription-plans-container {
- flex: 1;
- display: flex;
- flex-direction: column;
- align-items: stretch;
- justify-content: flex-start;
- padding: 20px;
- min-height: auto;
- overflow-y: visible;
- font-family: 'Outfit', sans-serif;
-}
-
-.plans-grid {
- gap: 20px;
- max-width: 100%;
- margin: 0;
- align-items: flex-start;
- padding: 20px 0 0 20px;
- overflow-x: auto;
- overflow-y: visible;
- }
-
- .plans-grid::-webkit-scrollbar {
- height: 8px;
- }
-
- .plans-grid::-webkit-scrollbar-track {
- background: #f1f1f1;
- border-radius: 4px;
- }
-
- .plans-grid::-webkit-scrollbar-thumb {
- background: #c1c1c1;
- border-radius: 4px;
- }
-
- .plans-grid::-webkit-scrollbar-thumb:hover {
- background: #a8a8a8;
- }
-
- @media (max-width: 1200px) {
- .plans-grid {
- gap: 15px;
- padding: 15px;
- }
- }
-
- @media (max-width: 768px) {
- .plans-grid {
- flex-direction: column;
- align-items: center;
- gap: 20px;
- overflow-x: visible;
- overflow-y: auto;
- }
-}
-
- /* Plan Card Styles */
-.plan-card {
- background: ${this.theme === Theme.DARK ? 'transparent' : '#ffffff'};
- border: ${this.theme === Theme.DARK ? '1px solid #e6e6e6' : '2px solid #e6e6e6'};
- border-radius: 4px;
- padding: 26px 24px;
- box-shadow: none;
- min-width: 290px;
- max-width: 350px;
- width: 350px;
- flex: 1;
- justify-content: flex-start;
- min-height: auto;
- max-height: none;
- overflow: visible;
- min-height: 348px;
- font-family: 'Outfit', sans-serif;
- position: relative;
- margin-top :30px
-
- }
-
- .plan-card.highlighted {
- border: ${this.theme === Theme.DARK ? '2px solid #ffffff' : '2px solid #000000'};
- box-shadow: 0 0 0 0px #000000 !important;
- }
-
- .plan-card:hover {
- box-shadow: none;
- }
-
-
- @media (max-width: 768px) {
- .plan-card {
- min-width: 50%;
- max-width: 400px;
- width: 100%;
- padding: 30px 20px;
- }
- }
-
- /* Popular Badge */
-.popular-badge {
- position: absolute;
- top: -12px;
- right: 20px;
- background: #4d4d4d;
- color: #ffffff;
- padding: 6px 16px;
- border-radius: 20px;
- font-size: 12px;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- z-index: 100;
- transition: all 0.3s ease;
- pointer-events: none;
- }
-
- /* Popular badge stays in place on hover */
- .plan-card:hover .popular-badge {
- z-index: 101;
- }
-
- /* For popular cards, badge scales with the card */
- .plan-card.popular:hover .popular-badge {
- transform: scale(1.02);
- z-index: 101;
- }
-
- /* Plan Title */
-.plan-title {
- font-size: 28px;
- font-weight: 700;
- color: #333333;
- }
-
- @media (max-width: 768px) {
- .plan-title {
- font-size: 24px;
- }
-}
-
- /* Plan Price */
- .plan-price .price-container {
- gap: 6px;
- }
-
- .plan-price .price-number {
- font-size: 39px;
- font-weight: 700;
- color: #4d4d4d;
- line-height: 1;
- }
-
- @media (max-width: 768px) {
- .plan-price .price-number {
- font-size: 42px;
- }
- }
-
- .plan-price .price-currency {
- font-size: 16px;
- font-weight: 400;
- color: #666666;
- line-height: 1;
- margin-top: 4px;
- margin-left: 4px;
- }
-
- @media (max-width: 768px) {
- .plan-price .price-currency {
- font-size: 14px;
- }
- }
-
- .plan-price .price-period {
- font-size: 18px;
- color: #666666;
- font-weight: 500;
- }
-
- @media (max-width: 768px) {
- .plan-price .price-period {
- font-size: 16px;
- }
- }
-
- /* Included Resources */
- .included-resources .resource-boxes {
- margin-top: 6px;
- }
-
- .included-resources .resource-box {
- border-radius: 4px;
- padding: 4px 2px;
- font-size: 14px;
- font-weight: 600;
- color: #4d4d4d;
- text-align: left;
- }
-
- /* Section Title */
-.section-title {
- font-size: 18px;
- font-weight: 600;
- color: #333333;
- margin: 0 0 8px 0;
-}
-
- /* Plan Features */
-.plan-features {
- list-style: none;
- }
-
- .plan-features .feature-item {
- padding: 4px 0 !important;
- margin-bottom: 0px !important;
- color: #4d4d4d;
- font-size: 14px;
- font-weight: 600;
- }
-
- .plan-features .feature-icon {
- font-weight: bold;
- font-size: 14px;
- color: #22c55e;
- }
-
- /* Plan Button */
-.plan-button {
- width: 65%;
- padding: 6px 6px;
- border-radius: 4px;
- font-size: 15px;
- font-weight: 400;
- font-family: 'Outfit', sans-serif;
- cursor: pointer;
- transition: all 0.3s ease;
- border: 1px solid;
- margin-top: auto;
- }
-
- .plan-button.primary {
- background: #4d4d4d;
- color: #ffffff;
- border-color: #4d4d4d;
- font-weight: 700;
- }
-
- .plan-button.primary:hover {
- background: #333333;
- border-color: #333333;
- }
-
- /* Disabled state */
- .plan-button.plan-button-disabled,
- .plan-button:disabled {
- opacity: 0.7 !important;
- cursor: not-allowed !important;
- pointer-events: none !important;
- }
-
- .plan-button.secondary {
- background: #ffffff;
- color: #4d4d4d;
- border-color: #4d4d4d;
- }
-
- .plan-button.secondary:hover {
- background: #f8f9fa;
- }
-
- @media (max-width: 768px) {
- .plan-button {
- width: auto;
- padding: 8px 28px;
- font-size: 16px;
-
- }
-}
-
- /* Plan Button Hidden */
-.plan-button-hidden {
- padding: 16px 32px;
- border-radius: 12px;
- font-size: 18px;
- font-weight: 600;
- font-family: 'Outfit', sans-serif;
- background: #f8f9fa;
- color: #6c757d;
- border: 2px solid #e9ecef;
- margin-top: auto;
- cursor: not-allowed;
- }
-
- @media (max-width: 768px) {
- .plan-button-hidden {
- padding: 14px 28px;
- font-size: 16px;
- }
-}
- *{
- box-sizing: border-box;
- font-family: 'Inter', sans-serif;
- -webkit-font-smoothing: antialiased;
- color: ${this.theme === 'dark' ? '#ffffff' : ''}!important;
- }
-
- /* Divider */
-.divider {
- height: 1px;
- background: #e0e0e0;
-}
-
-
- `;
-
- document.head.appendChild(style);
+ this.subscriptionRenderer.injectSubscriptionStyles(this.themeService.isDark());
}
private addButtonsToReferenceElement(element): void {
- this.selectWidgetData$
+ this.store
.pipe(
+ select(selectWidgetData),
filter((e) => !!e),
take(1)
)
@@ -911,16 +462,16 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
const totalButtons = widgetDataArray.length;
if (totalButtons > 0 && this.showSkeleton) {
- this.removeSkeletonLoader(element);
- this.appendSkeletonLoader(element, totalButtons);
+ this.domBuilder.removeSkeletonLoader(this.renderer, element);
+ this.domBuilder.appendSkeletonLoader(this.renderer, element);
} else if (totalButtons > 0 && !this.showSkeleton) {
- this.removeSkeletonLoader(element);
+ this.domBuilder.removeSkeletonLoader(this.renderer, element);
}
if (totalButtons === 0) {
if (this.showSkeleton) {
this.showSkeleton = false;
- this.removeSkeletonLoader(element);
+ this.domBuilder.removeSkeletonLoader(this.renderer, element);
}
if (!this.createAccountTextAppended) {
this.appendCreateAccountText(element);
@@ -934,7 +485,7 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
const fallbackTimeout = setTimeout(() => {
if (this.showSkeleton && !this.createAccountTextAppended) {
this.showSkeleton = false;
- this.removeSkeletonLoader(element);
+ this.domBuilder.removeSkeletonLoader(this.renderer, element);
const allButtons = element.querySelectorAll('button');
allButtons.forEach((button) => {
button.style.visibility = 'visible';
@@ -948,8 +499,8 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
const immediateFallback = setTimeout(() => {
if (this.showSkeleton && !this.createAccountTextAppended) {
this.showSkeleton = false;
- this.removeSkeletonLoader(element);
- this.forceRemoveAllSkeletonLoaders();
+ this.domBuilder.removeSkeletonLoader(this.renderer, element);
+ this.domBuilder.forceRemoveAllSkeletonLoaders(this.renderer, this.referenceElement);
const allButtons = element.querySelectorAll('button');
allButtons.forEach((button) => {
button.style.visibility = 'visible';
@@ -1060,342 +611,84 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
}
}
});
- }
-
- /**
- * Maps ui_preferences.border_radius to CSS value.
- * Values: 'none' | 'small' | 'medium' | 'large' -> 0 | 4px | 8px | 12px
- */
- private getBorderRadiusCssValue(borderRadius?: string): string {
- if (this.version !== SendOtpCenterVersion.V2) {
- return '8px';
- }
- switch (borderRadius) {
- case 'none':
- return '0';
- case 'small':
- return '4px';
- case 'medium':
- return '8px';
- case 'large':
- return '12px';
- default:
- return '8px';
- }
- }
-
- /**
- * Returns primary color from ui_preferences for the current effective theme.
- * If theme is 'system', resolves via prefers-color-scheme.
- */
- private getPrimaryColorForCurrentTheme(uiPreferences?: {
- light_theme_primary_color?: string;
- dark_theme_primary_color?: string;
- }): string {
- const isDark =
- this.theme === Theme.DARK ||
- (this.theme === Theme.SYSTEM &&
- typeof window !== 'undefined' &&
- window.matchMedia('(prefers-color-scheme: dark)').matches);
- if (this.version !== SendOtpCenterVersion.V2) {
- return isDark ? '#FFFFFF' : '#000000';
- }
- return isDark
- ? uiPreferences?.dark_theme_primary_color ?? '#FFFFFF'
- : uiPreferences?.light_theme_primary_color ?? '#000000';
- }
-
- private createLogoElement(logoUrl: string): HTMLElement | null {
- if (!logoUrl) {
- return null;
- }
- const wrapper: HTMLElement = this.renderer.createElement('div');
- wrapper.style.cssText = `
- width: 316px;
- display: flex;
- justify-content: center;
- margin: 0 8px 12px 8px;
- `;
- const img: HTMLImageElement = this.renderer.createElement('img');
- img.src = logoUrl;
- img.alt = 'Logo';
- img.loading = 'lazy';
- img.style.cssText = `
- max-height: 48px;
- max-width: 200px;
- object-fit: contain;
- `;
- this.renderer.appendChild(wrapper, img);
- return wrapper;
- }
-
- public appendPasswordAuthenticationButtonV2(element, buttonsData, totalButtons: number): void {
- if (this.showSkeleton) {
- this.showSkeleton = false;
- this.removeSkeletonLoader(element);
- }
- const selectWidgetTheme = this.getValueFromObservable(this.selectWidgetTheme$);
- const borderRadius = this.getBorderRadiusCssValue(selectWidgetTheme?.ui_preferences?.border_radius);
- const primaryColor = this.getPrimaryColorForCurrentTheme(selectWidgetTheme?.ui_preferences);
-
- const loginContainer: HTMLElement = this.renderer.createElement('div');
- loginContainer.style.cssText = `
- width: 316px;
- padding: 0;
- margin: 0 8px 16px 8px;
- display: flex;
- flex-direction: column;
- gap: 8px;
- box-sizing: border-box;
- font-family: 'Inter', sans-serif;
- border-radius: ${borderRadius};
- `;
-
- const title: HTMLElement = this.renderer.createElement('div');
- title.textContent = selectWidgetTheme?.ui_preferences?.title;
- title.style.cssText = `
- font-size: 16px;
- line-height: 20px;
- font-weight: 600;
- color: ${primaryColor};
- margin: 0 8px 20px 8px;
- text-align: center;
- width: 316px;
- `;
-
- const usernameField = this.renderer.createElement('div');
- usernameField.style.cssText = `
- display: flex;
- flex-direction: column;
- gap: 6px;
- `;
-
- const usernameLabel: HTMLElement = this.renderer.createElement('label');
- usernameLabel.textContent = 'Email or Mobile';
- usernameLabel.style.cssText = `
- font-size: 14px;
- font-weight: 500;
- color: ${this.theme === 'dark' ? '#e5e7eb' : '#5d6164'};
- `;
-
- const usernameInput: HTMLInputElement = this.renderer.createElement('input');
- usernameInput.type = 'text';
- usernameInput.placeholder = 'Email or Mobile';
- usernameInput.autocomplete = 'off';
- usernameInput.style.cssText = `
- width: 100%;
- height: 44px;
- padding: 0 16px;
- border: ${this.theme === 'dark' ? '1px solid #ffffff' : '1px solid #cbd5e1'};
- border-radius: ${borderRadius};
- background: ${this.theme === 'dark' ? 'transparent' : '#ffffff'};
- color: ${this.theme === 'dark' ? '#ffffff' : '#1f2937'};
- font-size: 14px;
- outline: none;
- box-sizing: border-box;
- `;
-
- const usernameNote: HTMLElement = this.renderer.createElement('p');
- usernameNote.textContent = 'Note: Please enter your Mobile number with the country code (e.g. 91)';
- const noteColor = this.version === 'v2' ? primaryColor : this.theme === 'dark' ? '#e5e7eb' : '#5d6164';
- usernameNote.style.cssText = `
- font-size: 12px;
- line-height: 18px;
- color: ${noteColor};
- margin: 0;
- `;
-
- const passwordField = this.renderer.createElement('div');
- passwordField.style.cssText = `
- display: flex;
- flex-direction: column;
- gap: 6px;
- position: relative;
- `;
-
- const passwordLabel: HTMLElement = this.renderer.createElement('label');
- passwordLabel.textContent = 'Password';
- passwordLabel.style.cssText = usernameLabel.style.cssText;
-
- const passwordInputWrapper = this.renderer.createElement('div');
- passwordInputWrapper.style.cssText = `
- position: relative;
- display: flex;
- align-items: center;
- `;
-
- const passwordInput: HTMLInputElement = this.renderer.createElement('input');
- passwordInput.type = 'password';
- passwordInput.placeholder = 'Password';
- passwordInput.autocomplete = 'off';
- passwordInput.style.cssText = `
- width: 100%;
- height: 44px;
- padding: 0 44px 0 16px;
- border: ${this.theme === 'dark' ? '1px solid #ffffff' : '1px solid #cbd5e1'};
- border-radius: ${borderRadius};
- background: ${this.theme === 'dark' ? 'transparent' : '#ffffff'};
- color: ${this.theme === 'dark' ? '#ffffff' : '#1f2937'};
- font-size: 14px;
- outline: none;
- box-sizing: border-box;
- `;
- this.addPasswordVisibilityToggle(passwordInput, passwordInputWrapper);
- this.renderer.appendChild(passwordInputWrapper, passwordInput);
-
- const hcaptchaWrapper: HTMLElement = this.renderer.createElement('div');
- hcaptchaWrapper.style.cssText = `
- width: 100%;
- display: flex;
- justify-content: center;
- padding: 8px 0;
- box-sizing: border-box;
- background: ${this.theme === 'dark' ? 'transparent' : 'transparent'};
- `;
- const hcaptchaPlaceholder: HTMLElement = this.renderer.createElement('div');
- hcaptchaPlaceholder.style.cssText = `
- display: inline-block;
- background: ${this.theme === 'dark' ? 'transparent' : 'transparent'};
- border-radius: ${borderRadius};
- `;
- this.renderer.appendChild(hcaptchaWrapper, hcaptchaPlaceholder);
-
- let hCaptchaToken: string = '';
- let hCaptchaWidgetId: any = null;
-
- const errorText: HTMLElement = this.renderer.createElement('div');
- errorText.style.cssText = `
- color: #d14343;
- font-size: 14px;
- min-height: 16px;
- display: none;
- margin-top: -4px;
- `;
-
- const loginButton: HTMLButtonElement = this.renderer.createElement('button');
- loginButton.textContent = 'Sign in';
- const isV2 = this.version === SendOtpCenterVersion.V2;
- const buttonColor = isV2 ? selectWidgetTheme?.ui_preferences?.button_color || '#3f51b5' : '#3f51b5';
- const buttonHoverColor = isV2 ? selectWidgetTheme?.ui_preferences?.button_hover_color || '#303f9f' : '#303f9f';
- const buttonTextColor = isV2 ? selectWidgetTheme?.ui_preferences?.button_text_color || '#ffffff' : '#ffffff';
- loginButton.style.cssText = `
- height: 36px;
- padding: 0 12px;
- background-color: ${buttonColor};
- color: ${buttonTextColor};
- border: none;
- border-radius: ${borderRadius};
- font-size: 14px;
- font-weight: 600;
- cursor: pointer;
- width: 100%;
- box-shadow: 0 1px 2px rgba(0,0,0,0.08);
- margin-top: 4px;
- `;
- loginButton.addEventListener('mouseenter', () => {
- if (buttonHoverColor) loginButton.style.backgroundColor = buttonHoverColor;
- });
- loginButton.addEventListener('mouseleave', () => {
- if (buttonColor) loginButton.style.backgroundColor = buttonColor;
- });
-
- const forgotPasswordWrapper: HTMLElement = this.renderer.createElement('div');
- forgotPasswordWrapper.style.cssText = `
- width: 100%;
- display: flex;
- justify-content: flex-end;
- margin-top: 4px;
- `;
- const forgotPasswordLink: HTMLAnchorElement = this.renderer.createElement('a');
- forgotPasswordLink.href = 'javascript:void(0)';
- forgotPasswordLink.textContent = 'Forgot Password?';
- forgotPasswordLink.style.cssText = `
- font-size: 13px;
- font-weight: 400;
- color: #007BFF;
- text-decoration: none;
- `;
-
- // Forgot password click handler - opens the dialog with forgot password flow
- forgotPasswordLink.addEventListener('click', () => {
- const userValue = usernameInput.value?.trim() || '';
- this.openForgotPasswordDialog(userValue);
- });
- this.renderer.appendChild(forgotPasswordWrapper, forgotPasswordLink);
-
- const resetHCaptcha = () => {
- const instance = this.getHCaptchaInstance();
- if (instance && hCaptchaWidgetId !== null && hCaptchaWidgetId !== undefined) {
- instance.reset(hCaptchaWidgetId);
- }
- hCaptchaToken = '';
- };
-
- const renderHCaptcha = () => {
- const instance = this.getHCaptchaInstance();
- if (!instance || !environment.hCaptchaSiteKey) {
- this.setInlineLoginError(errorText, 'Unable to load hCaptcha. Please refresh and try again.');
- return;
- }
- hcaptchaPlaceholder.innerHTML = '';
- hCaptchaWidgetId = instance.render(hcaptchaPlaceholder, {
- sitekey: environment.hCaptchaSiteKey,
- theme: this.theme === 'dark' ? 'dark' : 'light',
- callback: (token: string) => {
- hCaptchaToken = token;
- this.setInlineLoginError(errorText, '');
- },
- 'expired-callback': () => {
- hCaptchaToken = '';
- },
- 'error-callback': () => {
- hCaptchaToken = '';
- this.setInlineLoginError(errorText, 'hCaptcha verification failed. Please retry.');
- },
- });
- };
+ }
- this.ensureHCaptchaScriptLoaded(renderHCaptcha);
+ /**
+ * Maps ui_preferences.border_radius to CSS value.
+ * Values: 'none' | 'small' | 'medium' | 'large' -> 0 | 4px | 8px | 12px
+ */
+ private getBorderRadiusCssValue(borderRadius?: string): string {
+ if (this.version !== WidgetVersion.V2) {
+ return '8px';
+ }
+ switch (borderRadius) {
+ case 'none':
+ return '0';
+ case 'small':
+ return '4px';
+ case 'medium':
+ return '8px';
+ case 'large':
+ return '12px';
+ default:
+ return '8px';
+ }
+ }
- const submit = () =>
- this.handlePasswordAuthenticationLogin(
- buttonsData,
- usernameInput,
- passwordInput,
- errorText,
- loginButton,
- () => hCaptchaToken,
- resetHCaptcha
- );
+ /**
+ * Returns primary color from ui_preferences for the current effective theme.
+ * If theme is 'system', resolves via prefers-color-scheme.
+ */
+ private getPrimaryColorForCurrentTheme(uiPreferences?: {
+ light_theme_primary_color?: string;
+ dark_theme_primary_color?: string;
+ }): string {
+ const isDark = this.themeService.isDark();
+ if (this.version !== WidgetVersion.V2) {
+ return isDark ? '#FFFFFF' : '#000000';
+ }
+ return isDark
+ ? uiPreferences?.dark_theme_primary_color ?? '#FFFFFF'
+ : uiPreferences?.light_theme_primary_color ?? '#000000';
+ }
- loginButton.addEventListener('click', submit);
- [usernameInput, passwordInput].forEach((input) =>
- input.addEventListener('keydown', (event: KeyboardEvent) => {
- if (event.key === 'Enter') {
- event.preventDefault();
- submit();
- }
- })
- );
+ public appendPasswordAuthenticationButtonV2(element: HTMLElement, buttonsData: any, totalButtons: number): void {
+ if (this.showSkeleton) {
+ this.showSkeleton = false;
+ this.domBuilder.removeSkeletonLoader(this.renderer, element);
+ }
+ const selectWidgetTheme = this.widgetTheme() as any;
+ const borderRadius = this.getBorderRadiusCssValue(selectWidgetTheme?.ui_preferences?.border_radius);
+ const primaryColor = this.getPrimaryColorForCurrentTheme(selectWidgetTheme?.ui_preferences);
+ const isV2 = this.version === WidgetVersion.V2;
+ const buttonColor = isV2 ? selectWidgetTheme?.ui_preferences?.button_color || '#3f51b5' : '#3f51b5';
+ const buttonHoverColor = isV2 ? selectWidgetTheme?.ui_preferences?.button_hover_color || '#303f9f' : '#303f9f';
+ const buttonTextColor = isV2 ? selectWidgetTheme?.ui_preferences?.button_text_color || '#ffffff' : '#ffffff';
- // this.renderer.appendChild(usernameField, usernameLabel);
- this.renderer.appendChild(usernameField, usernameInput);
- this.renderer.appendChild(usernameField, usernameNote);
- // this.renderer.appendChild(passwordField, passwordLabel);
- this.renderer.appendChild(passwordField, passwordInputWrapper);
- this.renderer.appendChild(loginContainer, usernameField);
- this.renderer.appendChild(loginContainer, passwordField);
- this.renderer.appendChild(loginContainer, forgotPasswordWrapper);
- this.renderer.appendChild(loginContainer, hcaptchaWrapper);
- this.renderer.appendChild(loginContainer, errorText);
- this.renderer.appendChild(loginContainer, loginButton);
+ const loginContainer: HTMLElement = this.renderer.createElement('div');
+ loginContainer.style.cssText = `width:316px;padding:0;margin:0 8px 16px 8px;display:flex;flex-direction:column;gap:8px;box-sizing:border-box;font-family:'Inter',sans-serif;border-radius:${borderRadius};`;
- // Position login form based on input_fields setting
- const isInputFieldsTop = this.input_fields === 'top';
+ const title: HTMLElement = this.renderer.createElement('div');
+ title.textContent = selectWidgetTheme?.ui_preferences?.title;
+ title.style.cssText = `font-size:16px;line-height:20px;font-weight:600;color:${primaryColor};margin:0 8px 20px 8px;text-align:center;width:316px;`;
+
+ const loginButton: HTMLButtonElement = this.renderer.createElement('button');
+ loginButton.textContent = 'Sign in';
+ loginButton.style.cssText = `height:36px;padding:0 12px;background-color:${buttonColor};color:${buttonTextColor};border:none;border-radius:${borderRadius};font-size:14px;font-weight:600;cursor:pointer;width:100%;box-shadow:0 1px 2px rgba(0,0,0,0.08);margin-top:4px;`;
+ loginButton.addEventListener('mouseenter', () => {
+ loginButton.style.backgroundColor = buttonHoverColor;
+ });
+ loginButton.addEventListener('mouseleave', () => {
+ loginButton.style.backgroundColor = buttonColor;
+ });
+
+ const onForgotPassword = (email: string) => this.openForgotPasswordDialog(email);
+ this.buildLoginFields(loginContainer, buttonsData, loginButton, borderRadius, primaryColor, onForgotPassword);
- // Insert logo above the title if logo_url is available
+ const isInputFieldsTop = this.input_fields === 'top';
const logoUrl = selectWidgetTheme?.ui_preferences?.logo_url;
- const logoElement = this.createLogoElement(logoUrl);
+ const logoElement = this.domBuilder.createLogoElement(this.renderer, logoUrl);
+
if (logoElement) {
if (element.firstChild) {
this.renderer.insertBefore(element, logoElement, element.firstChild);
@@ -1404,7 +697,6 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
}
}
- // Always insert the "Login" title at the very top (after logo if present)
const logoOrFirst = logoElement ? logoElement.nextSibling : element.firstChild;
if (logoOrFirst) {
this.renderer.insertBefore(element, title, logoOrFirst);
@@ -1413,7 +705,6 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
}
if (isInputFieldsTop) {
- // input_fields = 'top': Login form (input fields) at top (after title), social buttons below
const titleNextSibling = title.nextSibling;
if (titleNextSibling) {
this.renderer.insertBefore(element, loginContainer, titleNextSibling);
@@ -1421,43 +712,12 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
this.renderer.appendChild(element, loginContainer);
}
} else {
- // input_fields = 'bottom': Social buttons at top (after title), login form at bottom
this.renderer.appendChild(element, loginContainer);
}
if (totalButtons > 1) {
- const dividerContainer: HTMLElement = this.renderer.createElement('div');
- dividerContainer.setAttribute('data-or-divider', 'true');
- dividerContainer.style.cssText = `
- display: flex;
- align-items: center;
- margin: 8px 8px 12px 8px;
- width: 316px;
- `;
- const dividerLineLeft: HTMLElement = this.renderer.createElement('div');
- dividerLineLeft.style.cssText = `
- flex: 1;
- height: 1px;
- background-color: #e0e0e0;
- `;
- const dividerText: HTMLElement = this.renderer.createElement('span');
- dividerText.textContent = 'Or continue with';
- dividerText.style.cssText = `
- padding: 0 12px;
- font-size: 12px;
- color: ${primaryColor};
- font-weight: 500;
- letter-spacing: 0.5px;
- `;
- const dividerLineRight: HTMLElement = this.renderer.createElement('div');
- dividerLineRight.style.cssText = dividerLineLeft.style.cssText;
-
- this.renderer.appendChild(dividerContainer, dividerLineLeft);
- this.renderer.appendChild(dividerContainer, dividerText);
- this.renderer.appendChild(dividerContainer, dividerLineRight);
-
+ const dividerContainer = this.domBuilder.createOrDivider(this.renderer, primaryColor);
if (isInputFieldsTop) {
- // input_fields = 'top': OR divider goes after login container
const nextSibling = loginContainer.nextSibling;
if (nextSibling) {
this.renderer.insertBefore(element, dividerContainer, nextSibling);
@@ -1465,7 +725,6 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
this.renderer.appendChild(element, dividerContainer);
}
} else {
- // input_fields = 'bottom': OR divider goes before login container
this.renderer.insertBefore(element, dividerContainer, loginContainer);
}
}
@@ -1485,16 +744,16 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
const hCaptchaToken = getHCaptchaToken();
if (!username || !password) {
- this.setInlineLoginError(errorText, 'Email/Mobile and password are required.');
+ this.domBuilder.setInlineError(errorText, 'Email/Mobile and password are required.');
return;
}
if (!hCaptchaToken) {
- this.setInlineLoginError(errorText, 'Please complete the hCaptcha verification.');
+ this.domBuilder.setInlineError(errorText, 'Please complete the hCaptcha verification.');
return;
}
- this.setInlineLoginError(errorText, '');
+ this.domBuilder.setInlineError(errorText, '');
const originalText = loginButton.textContent || 'Login';
loginButton.disabled = true;
loginButton.textContent = 'Please wait...';
@@ -1512,7 +771,7 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
loginButton.textContent = originalText;
if (res?.hasError) {
- this.setInlineLoginError(errorText, res?.errors?.[0] || 'Unable to login. Please try again.');
+ this.domBuilder.setInlineError(errorText, res?.errors?.[0] || 'Unable to login. Please try again.');
resetHCaptcha();
return;
}
@@ -1534,7 +793,7 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
return;
}
- this.setInlineLoginError(
+ this.domBuilder.setInlineError(
errorText,
error?.error?.errors?.[0] || 'Login failed. Please check your details and try again.'
);
@@ -1544,63 +803,19 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
);
}
- private setInlineLoginError(errorText: HTMLElement, message: string): void {
- errorText.textContent = message;
- errorText.style.display = message ? 'block' : 'none';
- }
-
- private addPasswordVisibilityToggle(input: HTMLInputElement, container: HTMLElement): void {
- let visible = false;
- const toggleBtn: HTMLButtonElement = this.renderer.createElement('button');
- toggleBtn.type = 'button';
- toggleBtn.style.cssText = `
- position: absolute;
- right: 12px;
- top: 50%;
- transform: translateY(-50%);
- border: none;
- background: transparent;
- cursor: pointer;
- padding: 0;
- margin: 0;
- width: 20px;
- height: 20px;
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 1;
- `;
-
- const hiddenIcon = ` `;
- const visibleIcon = ` `;
-
- const renderIcon = () => {
- toggleBtn.innerHTML = visible ? visibleIcon : hiddenIcon;
- };
- renderIcon();
-
- toggleBtn.addEventListener('click', () => {
- visible = !visible;
- input.type = visible ? 'text' : 'password';
- renderIcon();
- });
-
- this.renderer.appendChild(container, toggleBtn);
- }
-
/**
* Opens the forgot password dialog
*/
private openForgotPasswordDialog(prefillEmail: string = ''): void {
- // Open the dialog
this.ngZone.run(() => {
- this.show$ = of(true);
- // Signal to send-otp-center to open in forgot password mode
+ this.showForgotPassword.set(true);
this.otpWidgetService.openForgotPassword(prefillEmail);
+ this.cdr.detectChanges();
+ setTimeout(() => {
+ if (this.dialogPortalEl?.nativeElement && !this.dialogPortalRef) {
+ this.dialogPortalRef = this.widgetPortal.attach(this.dialogPortalEl.nativeElement);
+ }
+ });
});
}
@@ -1611,8 +826,9 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
// Clear the login container
loginContainer.innerHTML = '';
- const selectWidgetTheme = this.getValueFromObservable(this.selectWidgetTheme$);
+ const selectWidgetTheme = this.widgetTheme() as any;
const borderRadius = this.getBorderRadiusCssValue(selectWidgetTheme?.ui_preferences?.border_radius);
+ const isDarkFP = this.themeService.isDark();
// Create back button
const backButton: HTMLButtonElement = this.renderer.createElement('button');
@@ -1642,7 +858,7 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
font-size: 16px;
line-height: 20px;
font-weight: 600;
- color: ${this.theme === 'dark' ? '#ffffff' : '#1f2937'};
+ color: ${isDarkFP ? '#ffffff' : '#1f2937'};
margin-bottom: 16px;
`;
@@ -1658,16 +874,16 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
const emailInput: HTMLInputElement = this.renderer.createElement('input');
emailInput.type = 'text';
emailInput.placeholder = 'Email or Mobile';
- emailInput.value = prefillEmail;
emailInput.autocomplete = 'off';
+ emailInput.value = prefillEmail;
emailInput.style.cssText = `
width: 100%;
height: 44px;
padding: 0 16px;
- border: ${this.theme === 'dark' ? '1px solid #ffffff' : '1px solid #cbd5e1'};
+ border: ${isDarkFP ? '1px solid #ffffff' : '1px solid #cbd5e1'};
border-radius: ${borderRadius};
- background: ${this.theme === 'dark' ? 'transparent' : '#ffffff'};
- color: ${this.theme === 'dark' ? '#ffffff' : '#1f2937'};
+ background: ${isDarkFP ? 'transparent' : '#ffffff'};
+ color: ${isDarkFP ? '#ffffff' : '#1f2937'};
font-size: 14px;
outline: none;
box-sizing: border-box;
@@ -1704,11 +920,11 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
const handleSendOtp = () => {
const userDetails = emailInput.value?.trim();
if (!userDetails) {
- this.setInlineLoginError(errorText, 'Email or Mobile is required.');
+ this.domBuilder.setInlineError(errorText, 'Email or Mobile is required.');
return;
}
- this.setInlineLoginError(errorText, '');
+ this.domBuilder.setInlineError(errorText, '');
const originalText = sendOtpButton.textContent || 'Send OTP';
sendOtpButton.disabled = true;
sendOtpButton.textContent = 'Please wait...';
@@ -1724,7 +940,7 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
sendOtpButton.textContent = originalText;
if (res?.hasError) {
- this.setInlineLoginError(
+ this.domBuilder.setInlineError(
errorText,
res?.errors?.[0] || 'Unable to send OTP. Please try again.'
);
@@ -1737,7 +953,7 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
(error) => {
sendOtpButton.disabled = false;
sendOtpButton.textContent = originalText;
- this.setInlineLoginError(
+ this.domBuilder.setInlineError(
errorText,
error?.error?.errors?.[0] || 'Failed to send OTP. Please try again.'
);
@@ -1769,8 +985,9 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
// Clear the login container
loginContainer.innerHTML = '';
- const selectWidgetTheme = this.getValueFromObservable(this.selectWidgetTheme$);
+ const selectWidgetTheme = this.widgetTheme() as any;
const borderRadius = this.getBorderRadiusCssValue(selectWidgetTheme?.ui_preferences?.border_radius);
+ const isDarkCP = this.themeService.isDark();
let remainingSeconds = 15;
let timerInterval: any = null;
@@ -1804,7 +1021,7 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
font-size: 16px;
line-height: 20px;
font-weight: 600;
- color: ${this.theme === 'dark' ? '#ffffff' : '#1f2937'};
+ color: ${isDarkCP ? '#ffffff' : '#1f2937'};
margin-bottom: 8px;
`;
@@ -1812,7 +1029,7 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
const userInfo: HTMLElement = this.renderer.createElement('p');
userInfo.style.cssText = `
font-size: 14px;
- color: ${this.theme === 'dark' ? '#e5e7eb' : '#5d6164'};
+ color: ${isDarkCP ? '#e5e7eb' : '#5d6164'};
margin: 0 0 8px 0;
`;
userInfo.innerHTML = `${userDetails} Change `;
@@ -1897,10 +1114,10 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
width: 100%;
height: 44px;
padding: 0 16px;
- border: ${this.theme === 'dark' ? '1px solid #ffffff' : '1px solid #cbd5e1'};
+ border: ${isDarkCP ? '1px solid #ffffff' : '1px solid #cbd5e1'};
border-radius: ${borderRadius};
- background: ${this.theme === 'dark' ? 'transparent' : '#ffffff'};
- color: ${this.theme === 'dark' ? '#ffffff' : '#1f2937'};
+ background: ${isDarkCP ? 'transparent' : '#ffffff'};
+ color: ${isDarkCP ? '#ffffff' : '#1f2937'};
font-size: 14px;
outline: none;
box-sizing: border-box;
@@ -1927,7 +1144,7 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
'Password should contain at least one Capital Letter, one Small Letter, one Digit and one Symbol (min 8 characters)';
passwordHint.style.cssText = `
font-size: 12px;
- color: ${this.theme === 'dark' ? '#9ca3af' : '#6b7280'};
+ color: ${isDarkCP ? '#9ca3af' : '#6b7280'};
margin: -8px 0 12px 0;
`;
@@ -1966,30 +1183,30 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
const confirmPassword = confirmPasswordInput.value;
if (!otp) {
- this.setInlineLoginError(errorText, 'OTP is required.');
+ this.domBuilder.setInlineError(errorText, 'OTP is required.');
return;
}
if (!password) {
- this.setInlineLoginError(errorText, 'Password is required.');
+ this.domBuilder.setInlineError(errorText, 'Password is required.');
return;
}
if (password.length < 8) {
- this.setInlineLoginError(errorText, 'Password must be at least 8 characters.');
+ this.domBuilder.setInlineError(errorText, 'Password must be at least 8 characters.');
return;
}
if (!PASSWORD_REGEX.test(password)) {
- this.setInlineLoginError(
+ this.domBuilder.setInlineError(
errorText,
'Password should contain at least one Capital Letter, one Small Letter, one Digit and one Symbol.'
);
return;
}
if (password !== confirmPassword) {
- this.setInlineLoginError(errorText, 'Passwords do not match.');
+ this.domBuilder.setInlineError(errorText, 'Passwords do not match.');
return;
}
- this.setInlineLoginError(errorText, '');
+ this.domBuilder.setInlineError(errorText, '');
const originalText = submitButton.textContent || 'Submit';
submitButton.disabled = true;
submitButton.textContent = 'Please wait...';
@@ -2008,7 +1225,7 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
submitButton.textContent = originalText;
if (res?.hasError) {
- this.setInlineLoginError(
+ this.domBuilder.setInlineError(
errorText,
res?.errors?.[0] || 'Unable to reset password. Please try again.'
);
@@ -2022,7 +1239,7 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
(error) => {
submitButton.disabled = false;
submitButton.textContent = originalText;
- this.setInlineLoginError(
+ this.domBuilder.setInlineError(
errorText,
error?.error?.errors?.[0] || 'Failed to reset password. Please try again.'
);
@@ -2056,159 +1273,103 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
this.buildLoginFormContent(loginContainer, buttonsData);
}
- /**
- * Builds the login form content within the given container
- */
private buildLoginFormContent(loginContainer: HTMLElement, buttonsData: any): void {
- const selectWidgetTheme = this.getValueFromObservable(this.selectWidgetTheme$);
+ const selectWidgetTheme = this.widgetTheme() as any;
const borderRadius = this.getBorderRadiusCssValue(selectWidgetTheme?.ui_preferences?.border_radius);
const primaryColor = this.getPrimaryColorForCurrentTheme(selectWidgetTheme?.ui_preferences);
const title: HTMLElement = this.renderer.createElement('div');
title.textContent = 'Login';
- title.style.cssText = `
- font-size: 16px;
- line-height: 20px;
- font-weight: 600;
- color: ${this.theme === 'dark' ? '#ffffff' : '#1f2937'};
- margin-bottom: 0px;
- text-align: center;
- `;
+ title.style.cssText = `font-size:16px;line-height:20px;font-weight:600;color:${
+ this.themeService.isDark() ? '#ffffff' : '#1f2937'
+ };margin-bottom:0;text-align:center;`;
- const usernameField = this.renderer.createElement('div');
- usernameField.style.cssText = `
- display: flex;
- flex-direction: column;
- gap: 6px;
- `;
+ const loginButton: HTMLButtonElement = this.renderer.createElement('button');
+ loginButton.textContent = 'Login';
+ loginButton.style.cssText = `height:44px;padding:0 12px;background-color:#3f51b5;color:#ffffff;border:none;border-radius:${borderRadius};font-size:14px;font-weight:600;cursor:pointer;width:100%;box-shadow:0 1px 2px rgba(0,0,0,0.08);margin-top:4px;`;
+
+ const onForgotPassword = (email: string) => this.showForgotPasswordForm(loginContainer, buttonsData, email);
+
+ const logoUrl = selectWidgetTheme?.ui_preferences?.logo_url;
+ const logoElement = this.domBuilder.createLogoElement(this.renderer, logoUrl);
+ if (logoElement) {
+ this.renderer.appendChild(loginContainer, logoElement);
+ }
+ this.renderer.appendChild(loginContainer, title);
+
+ this.buildLoginFields(loginContainer, buttonsData, loginButton, borderRadius, primaryColor, onForgotPassword);
+ }
+
+ private buildLoginFields(
+ loginContainer: HTMLElement,
+ buttonsData: any,
+ loginButton: HTMLButtonElement,
+ borderRadius: string,
+ primaryColor: string,
+ onForgotPassword: (email: string) => void
+ ): void {
+ const isDark = this.themeService.isDark();
+ const noteColor = this.version === 'v2' ? primaryColor : isDark ? '#e5e7eb' : '#5d6164';
+
+ const usernameField: HTMLElement = this.renderer.createElement('div');
+ usernameField.style.cssText = 'display:flex;flex-direction:column;gap:6px;';
const usernameInput: HTMLInputElement = this.renderer.createElement('input');
usernameInput.type = 'text';
usernameInput.placeholder = 'Email or Mobile';
usernameInput.autocomplete = 'off';
- usernameInput.style.cssText = `
- width: 100%;
- height: 44px;
- padding: 0 16px;
- border: ${this.theme === 'dark' ? '1px solid #ffffff' : '1px solid #cbd5e1'};
- border-radius: ${borderRadius};
- background: ${this.theme === 'dark' ? 'transparent' : '#ffffff'};
- color: ${this.theme === 'dark' ? '#ffffff' : '#1f2937'};
- font-size: 14px;
- outline: none;
- box-sizing: border-box;
- `;
+ usernameInput.style.cssText = `width:100%;height:44px;padding:0 16px;border:1px solid ${
+ isDark ? '#ffffff' : '#cbd5e1'
+ };border-radius:${borderRadius};background:${isDark ? 'transparent' : '#ffffff'};color:${
+ isDark ? '#ffffff' : '#1f2937'
+ };font-size:14px;outline:none;box-sizing:border-box;`;
const usernameNote: HTMLElement = this.renderer.createElement('p');
usernameNote.textContent = 'Note: Please enter your Mobile number with the country code (e.g. 91)';
- const noteColor = this.version === 'v2' ? primaryColor : this.theme === 'dark' ? '#e5e7eb' : '#5d6164';
- usernameNote.style.cssText = `
- font-size: 12px;
- line-height: 18px;
- color: ${noteColor};
- margin: 0;
- `;
+ usernameNote.style.cssText = `font-size:12px;line-height:18px;color:${noteColor};margin:0;`;
- const passwordField = this.renderer.createElement('div');
- passwordField.style.cssText = `
- display: flex;
- flex-direction: column;
- gap: 6px;
- position: relative;
- `;
+ const passwordField: HTMLElement = this.renderer.createElement('div');
+ passwordField.style.cssText = 'display:flex;flex-direction:column;gap:6px;position:relative;';
- const passwordInputWrapper = this.renderer.createElement('div');
- passwordInputWrapper.style.cssText = `
- position: relative;
- display: flex;
- align-items: center;
- `;
+ const passwordInputWrapper: HTMLElement = this.renderer.createElement('div');
+ passwordInputWrapper.style.cssText = 'position:relative;display:flex;align-items:center;';
const passwordInput: HTMLInputElement = this.renderer.createElement('input');
passwordInput.type = 'password';
passwordInput.placeholder = 'Password';
passwordInput.autocomplete = 'off';
- passwordInput.style.cssText = `
- width: 100%;
- height: 44px;
- padding: 0 44px 0 16px;
- border: ${this.theme === 'dark' ? '1px solid #ffffff' : '1px solid #cbd5e1'};
- border-radius: ${borderRadius};
- background: ${this.theme === 'dark' ? 'transparent' : '#ffffff'};
- color: ${this.theme === 'dark' ? '#ffffff' : '#1f2937'};
- font-size: 14px;
- outline: none;
- box-sizing: border-box;
- `;
- this.addPasswordVisibilityToggle(passwordInput, passwordInputWrapper);
+ passwordInput.style.cssText = `width:100%;height:44px;padding:0 44px 0 16px;border:1px solid ${
+ isDark ? '#ffffff' : '#cbd5e1'
+ };border-radius:${borderRadius};background:${isDark ? 'transparent' : '#ffffff'};color:${
+ isDark ? '#ffffff' : '#1f2937'
+ };font-size:14px;outline:none;box-sizing:border-box;`;
+ this.domBuilder.addPasswordVisibilityToggle(
+ this.renderer,
+ passwordInput,
+ passwordInputWrapper,
+ this.themeService.resolvedTheme()
+ );
this.renderer.appendChild(passwordInputWrapper, passwordInput);
const hcaptchaWrapper: HTMLElement = this.renderer.createElement('div');
- hcaptchaWrapper.style.cssText = `
- width: 100%;
- display: flex;
- justify-content: center;
- padding: 8px 0;
- box-sizing: border-box;
- background: ${this.theme === 'dark' ? 'transparent' : 'transparent'};
- `;
+ hcaptchaWrapper.style.cssText =
+ 'width:100%;display:flex;justify-content:center;padding:8px 0;box-sizing:border-box;';
const hcaptchaPlaceholder: HTMLElement = this.renderer.createElement('div');
- hcaptchaPlaceholder.style.cssText = `
- display: inline-block;
- background: ${this.theme === 'dark' ? 'transparent' : 'transparent'};
- border-radius: ${borderRadius};
- `;
+ hcaptchaPlaceholder.style.cssText = `display:inline-block;border-radius:${borderRadius};`;
this.renderer.appendChild(hcaptchaWrapper, hcaptchaPlaceholder);
- let hCaptchaToken: string = '';
+ let hCaptchaToken = '';
let hCaptchaWidgetId: any = null;
- const errorText: HTMLElement = this.renderer.createElement('div');
- errorText.style.cssText = `
- color: #d14343;
- font-size: 14px;
- min-height: 16px;
- display: none;
- margin-top: -4px;
- `;
-
- const loginButton: HTMLButtonElement = this.renderer.createElement('button');
- loginButton.textContent = 'Login';
- loginButton.style.cssText = `
- height: 44px;
- padding: 0 12px;
- background-color: #3f51b5;
- color: #ffffff;
- border: none;
- border-radius: ${borderRadius};
- font-size: 14px;
- font-weight: 600;
- cursor: pointer;
- width: 100%;
- box-shadow: 0 1px 2px rgba(0,0,0,0.08);
- margin-top: 4px;
- `;
+ const errorText: HTMLElement = this.domBuilder.createErrorElement(this.renderer);
const forgotPasswordWrapper: HTMLElement = this.renderer.createElement('div');
- forgotPasswordWrapper.style.cssText = `
- width: 100%;
- display: flex;
- justify-content: flex-end;
- margin-top: 4px;
- `;
+ forgotPasswordWrapper.style.cssText = 'width:100%;display:flex;justify-content:flex-end;margin-top:4px;';
const forgotPasswordLink: HTMLAnchorElement = this.renderer.createElement('a');
forgotPasswordLink.href = 'javascript:void(0)';
forgotPasswordLink.textContent = 'Forgot Password?';
- forgotPasswordLink.style.cssText = `
- font-size: 13px;
- font-weight: 400;
- color: #1976d2;
- text-decoration: none;
- `;
- forgotPasswordLink.addEventListener('click', () => {
- const userValue = usernameInput.value?.trim() || '';
- this.showForgotPasswordForm(loginContainer, buttonsData, userValue);
- });
+ forgotPasswordLink.style.cssText = 'font-size:13px;font-weight:400;color:#1976d2;text-decoration:none;';
+ forgotPasswordLink.addEventListener('click', () => onForgotPassword(usernameInput.value?.trim() || ''));
this.renderer.appendChild(forgotPasswordWrapper, forgotPasswordLink);
const resetHCaptcha = () => {
@@ -2222,23 +1383,23 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
const renderHCaptcha = () => {
const instance = this.getHCaptchaInstance();
if (!instance || !environment.hCaptchaSiteKey) {
- this.setInlineLoginError(errorText, 'Unable to load hCaptcha. Please refresh and try again.');
+ this.domBuilder.setInlineError(errorText, 'Unable to load hCaptcha. Please refresh and try again.');
return;
}
hcaptchaPlaceholder.innerHTML = '';
hCaptchaWidgetId = instance.render(hcaptchaPlaceholder, {
sitekey: environment.hCaptchaSiteKey,
- theme: this.theme === 'dark' ? 'dark' : 'light',
+ theme: isDark ? WidgetTheme.Dark : WidgetTheme.Light,
callback: (token: string) => {
hCaptchaToken = token;
- this.setInlineLoginError(errorText, '');
+ this.domBuilder.setInlineError(errorText, '');
},
'expired-callback': () => {
hCaptchaToken = '';
},
'error-callback': () => {
hCaptchaToken = '';
- this.setInlineLoginError(errorText, 'hCaptcha verification failed. Please retry.');
+ this.domBuilder.setInlineError(errorText, 'hCaptcha verification failed. Please retry.');
},
});
};
@@ -2258,20 +1419,14 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
loginButton.addEventListener('click', submit);
[usernameInput, passwordInput].forEach((input) =>
- input.addEventListener('keydown', (event: KeyboardEvent) => {
- if (event.key === 'Enter') {
- event.preventDefault();
+ input.addEventListener('keydown', (e: KeyboardEvent) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
submit();
}
})
);
- const logoUrl = selectWidgetTheme?.ui_preferences?.logo_url;
- const logoElement = this.createLogoElement(logoUrl);
- if (logoElement) {
- this.renderer.appendChild(loginContainer, logoElement);
- }
- this.renderer.appendChild(loginContainer, title);
this.renderer.appendChild(usernameField, usernameInput);
this.renderer.appendChild(usernameField, usernameNote);
this.renderer.appendChild(passwordField, passwordInputWrapper);
@@ -2347,8 +1502,8 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
if (this.showSkeleton) {
this.showSkeleton = false;
- this.removeSkeletonLoader(element);
- this.forceRemoveAllSkeletonLoaders();
+ this.domBuilder.removeSkeletonLoader(this.renderer, element);
+ this.domBuilder.forceRemoveAllSkeletonLoaders(this.renderer, this.referenceElement);
const allButtons = element.querySelectorAll('button');
allButtons.forEach((button) => {
@@ -2365,10 +1520,10 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
private appendButton(element, buttonsData): void {
if (this.showSkeleton) {
this.showSkeleton = false;
- this.removeSkeletonLoader(element);
+ this.domBuilder.removeSkeletonLoader(this.renderer, element);
}
- const selectWidgetTheme = this.getValueFromObservable(this.selectWidgetTheme$);
+ const selectWidgetTheme = this.widgetTheme() as any;
const borderRadius = this.getBorderRadiusCssValue(selectWidgetTheme?.ui_preferences?.border_radius);
const button: HTMLButtonElement = this.renderer.createElement('button');
@@ -2411,6 +1566,8 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
}
}
+ button.setAttribute('data-paw-button', 'true');
+ button.setAttribute('data-paw-icon-only', 'true');
button.style.cssText = `
outline: none;
padding: 12px;
@@ -2419,7 +1576,7 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
justify-content: center;
font-size: 14px;
background-color: transparent;
- border: ${this.theme === 'dark' ? '1px solid #ffffff' : '1px solid #d1d5db'};
+ border: ${this.themeService.isDark() ? '1px solid #ffffff' : '1px solid #d1d5db'};
border-radius: ${borderRadius};
cursor: pointer;
visibility: ${isOtpButton ? 'hidden' : 'visible'};
@@ -2452,6 +1609,7 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
} else {
const span: HTMLSpanElement = this.renderer.createElement('span');
+ button.setAttribute('data-paw-button', 'true');
button.style.cssText = `
outline: none;
padding: 0 16px;
@@ -2461,10 +1619,10 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
${useDiv ? '' : 'gap: 12px;'}
font-size: 14px;
background-color: transparent;
- border: ${this.theme === 'dark' ? '1px solid #ffffff' : '1px solid #000000'};
+ border: ${this.themeService.isDark() ? '1px solid #ffffff' : '1px solid #000000'};
border-radius: ${borderRadius};
height: 44px;
- color: ${this.theme === 'dark' ? '#ffffff' : '#111827'};
+ color: ${this.themeService.isDark() ? '#ffffff' : '#111827'};
margin: 8px 8px 16px 8px;
cursor: pointer;
width: ${useDiv ? '316px' : '260px'};
@@ -2477,7 +1635,7 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
${invertIcon ? 'filter: invert(1);' : ''}
`;
span.style.cssText = `
- color: ${this.theme === 'dark' ? '#ffffff' : '#111827'};
+ color: ${this.themeService.isDark() ? '#ffffff' : '#111827'};
font-weight: 600;
`;
image.src = buttonsData.icon;
@@ -2505,7 +1663,6 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
align-items: center;
justify-content: flex-start;
gap: 12px;
- width: 180px;
`;
this.renderer.appendChild(contentDiv, image);
this.renderer.appendChild(contentDiv, span);
@@ -2547,7 +1704,7 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
}
this.createAccountTextAppended = true;
- const selectWidgetTheme = this.getValueFromObservable(this.selectWidgetTheme$);
+ const selectWidgetTheme = this.widgetTheme() as any;
const primaryColor = this.getPrimaryColorForCurrentTheme(selectWidgetTheme?.ui_preferences);
const paragraph: HTMLParagraphElement = this.renderer.createElement('p');
@@ -2608,7 +1765,7 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
(error: HttpErrorResponse) => {
if (error?.status === 403) {
this.setShowRegistration(true);
- this.show$ = of(true);
+ this.show.set(true);
this.registrationViaLogin = false;
}
}
@@ -2620,22 +1777,25 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
if (this.registrationViaLogin) {
if (value) {
this.setShowLogin(false);
- this.show$ = of(true);
+ this.show.set(true);
} else {
+ // Detach portal when closing
+ this.dialogPortalRef?.detach();
+ this.dialogPortalRef = null;
// When closing registration, go back to where user came from
if (this.cameFromLogin) {
// If user came from login, go back to login
this.setShowLogin(true);
- this.show$ = of(true);
+ this.show.set(true);
} else if (this.cameFromSendOtpCenter) {
// If user came from send-otp-center, go back to send-otp-center
- // Only close login without affecting show$ - avoid race condition
+ // Only close login without affecting show - avoid race condition
this.otpWidgetService.openLogin(false);
- this.show$ = of(true);
+ this.show.set(true);
} else {
// If user came from dynamically appended buttons, just close without opening anything
this.setShowLogin(false);
- this.show$ = of(false);
+ this.show.set(false);
}
// Reset the flags
this.cameFromLogin = false;
@@ -2644,21 +1804,36 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
} else {
this.setShowLogin(false);
if (this.referenceElement) {
- this.show$ = of(value);
+ this.show.set(value);
}
}
- this.showRegistration.next(value);
+ this.showRegistration.set(value);
if (data) {
this.prefillDetails = data;
}
+ if (value) {
+ this.cdr.detectChanges();
+ if (this.dialogPortalEl?.nativeElement && !this.dialogPortalRef) {
+ this.dialogPortalRef = this.widgetPortal.attach(this.dialogPortalEl.nativeElement);
+ }
+ }
});
}
public setShowLogin(value: boolean) {
this.ngZone.run(() => {
if (this.referenceElement) {
- this.show$ = of(value);
+ this.show.set(value);
}
this.otpWidgetService.openLogin(value);
+ if (value) {
+ this.cdr.detectChanges();
+ if (this.dialogPortalEl?.nativeElement && !this.dialogPortalRef) {
+ this.dialogPortalRef = this.widgetPortal.attach(this.dialogPortalEl.nativeElement);
+ }
+ } else {
+ this.dialogPortalRef?.detach();
+ this.dialogPortalRef = null;
+ }
});
}
@@ -2685,123 +1860,6 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
}
}
- private appendSkeletonLoader(element, buttonCount: number): void {
- const skeletonContainer = this.renderer.createElement('div');
- skeletonContainer.id = 'skeleton-loader';
- skeletonContainer.style.cssText = `
- display: block;
- width: 100%;
- `;
-
- for (let i = 0; i < 3; i++) {
- const skeletonButton = this.renderer.createElement('div');
- skeletonButton.style.cssText = `
- width: 230px;
- height: 40px;
- background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
- background-size: 200% 100%;
- animation: skeleton-loading 1.5s infinite;
- border-radius: 4px;
- margin: 8px 8px 16px 8px;
- display: block;
- box-sizing: border-box;
- `;
-
- if (!document.getElementById('skeleton-animation')) {
- const style = this.renderer.createElement('style');
- style.id = 'skeleton-animation';
- style.textContent = `
- @keyframes skeleton-loading {
- 0% { background-position: 200% 0; }
- 100% { background-position: -200% 0; }
- }
- `;
- document.head.appendChild(style);
- }
-
- this.renderer.appendChild(skeletonContainer, skeletonButton);
- }
-
- this.renderer.appendChild(element, skeletonContainer);
- }
-
- private removeSkeletonLoader(element): void {
- const skeletonLoader = element.querySelector('#skeleton-loader');
- if (skeletonLoader) {
- this.renderer.removeChild(element, skeletonLoader);
- }
-
- // Also remove any skeleton loaders that might be in the element
- const allSkeletonLoaders = element.querySelectorAll('#skeleton-loader');
- allSkeletonLoaders.forEach((loader) => {
- if (loader.parentNode) {
- this.renderer.removeChild(element, loader);
- }
- });
-
- this.forceRemoveAllSkeletonLoaders();
- }
-
- private forceRemoveAllSkeletonLoaders(): void {
- // Remove skeleton loaders from the reference element
- if (this.referenceElement) {
- const skeletonLoaders = this.referenceElement.querySelectorAll('#skeleton-loader');
- skeletonLoaders.forEach((loader, index) => {
- this.renderer.removeChild(this.referenceElement, loader);
- });
- }
-
- // Also try to remove from document body (fallback)
- const globalSkeletonLoaders = document.querySelectorAll('#skeleton-loader');
- if (globalSkeletonLoaders.length > 0) {
- globalSkeletonLoaders.forEach((loader, index) => {
- if (loader.parentNode) {
- loader.parentNode.removeChild(loader);
- }
- });
- }
- }
- private formatSubscriptionPlans(plans: any[]): any[] {
- return plans.map((plan, index) => ({
- id: plan.plan_name?.toLowerCase().replace(/\s+/g, '-') || `plan-${index}`,
- title: plan.plan_name || 'Unnamed Plan',
- priceNumber: this.extractPriceValue(plan.plan_price) || 0,
- priceText:
- this.extractCurrency(plan.plan_price) ||
- (plan.plan_price ? plan.plan_price.replace(/[\d.]/g, '').trim() : 'Free'),
- priceValue: this.extractPriceValue(plan.plan_price),
- currency: this.extractCurrency(plan.plan_price),
- buttonText: plan.subscribe_button_hidden ? 'Hidden' : 'Get Started',
- buttonStyle: 'secondary', // All plans use secondary style
- isPopular: false, // No plan is popular by default
- isSelected: false, // No plan is selected by default
- features: this.getIncludedFeatures(plan.charges),
- status: plan.plan_status,
- subscribeButtonLink: this.isLogin
- ? plan.subscribe_button_link?.replace('{ref_id}', this.referenceId)
- : this.loginRedirectUrl,
- subscribeButtonHidden: plan.subscribe_button_hidden,
- }));
- }
- private extractPriceValue(priceString: string): number {
- if (!priceString) return 0;
- const match = priceString.match(/[\d.]+/);
- return match ? parseFloat(match[0]) : 0;
- }
-
- private extractCurrency(priceString: string): string {
- if (!priceString) return '';
- const match = priceString.match(/[A-Z]{3}/);
- return match ? match[0] : '';
- }
- private getIncludedFeatures(charges: any[]): string[] {
- if (!charges || !Array.isArray(charges)) return [];
- return charges.map((charge) => {
- const quota = charge.quotas || '';
- const metricName = charge.billable_metric_name || '';
- return `${quota} ${metricName}`.trim();
- });
- }
public handleSubscriptionToggle(event?: any): void {
if (this.isPreview) {
this.toggleSendOtp();
@@ -2848,9 +1906,43 @@ export class SendOtpComponent extends BaseComponent implements OnInit, OnDestroy
}
}
+ private reapplyInjectedButtonTheme(dark: boolean): void {
+ const container = this.referenceElement;
+ if (!container) return;
+
+ const selectWidgetTheme = this.widgetTheme() as any;
+ const primaryColor = this.getPrimaryColorForCurrentTheme(selectWidgetTheme?.ui_preferences);
+ const textColor = dark ? '#ffffff' : '#111827';
+ const border = dark ? '1px solid #ffffff' : '1px solid #000000';
+ const borderIconOnly = dark ? '1px solid #ffffff' : '1px solid #d1d5db';
+
+ container.querySelectorAll('button[data-paw-button]').forEach((btn) => {
+ const isIconOnly = btn.hasAttribute('data-paw-icon-only');
+ if (isIconOnly) {
+ btn.style.border = borderIconOnly;
+ } else {
+ btn.style.border = border;
+ btn.style.color = textColor;
+ const span = btn.querySelector('span');
+ if (span) span.style.color = textColor;
+ const img = btn.querySelector('img');
+ if (img) {
+ const isApple = img.alt?.toLowerCase().includes('apple');
+ const isPassword = btn.dataset['serviceId'] === String(FeatureServiceIds.PasswordAuthentication);
+ img.style.filter = dark && (isApple || isPassword) ? 'invert(1)' : '';
+ }
+ }
+ });
+
+ const createAccountP = container.querySelector('p[data-create-account="true"]');
+ if (createAccountP) {
+ createAccountP.style.setProperty('color', primaryColor, 'important');
+ }
+ }
+
private shouldInvertIcon(buttonsData: any): boolean {
const isApple = buttonsData?.text?.toLowerCase()?.includes('apple');
const isPassword = buttonsData?.service_id === FeatureServiceIds.PasswordAuthentication;
- return this.theme === Theme.DARK && (isApple || isPassword);
+ return this.themeService.isDark() && (isApple || isPassword);
}
}
diff --git a/apps/proxy-auth-element/src/assets/.gitkeep b/apps/36-blocks-widget/src/assets/.gitkeep
similarity index 100%
rename from apps/proxy-auth-element/src/assets/.gitkeep
rename to apps/36-blocks-widget/src/assets/.gitkeep
diff --git a/apps/36-blocks-widget/src/assets/scss/component/_form-field.scss b/apps/36-blocks-widget/src/assets/scss/component/_form-field.scss
new file mode 100644
index 00000000..8beb483b
--- /dev/null
+++ b/apps/36-blocks-widget/src/assets/scss/component/_form-field.scss
@@ -0,0 +1,118 @@
+// // Default theme changes
+// .mat-form-field {
+// .mat-form-field-wrapper {
+// .mat-form-field-subscript-wrapper {
+// margin-top: 5px;
+// padding-left: 0px;
+// .mat-error {
+// font-size: 12px;
+// }
+// }
+// .mat-form-field-flex {
+// font-size: 14px;
+// line-height: 1.225;
+// .mat-form-field-infix {
+// .mat-form-field-label-wrapper {
+// .mat-form-field-label {
+// color: var(--color-common-slate);
+// }
+// }
+// }
+// .mat-form-field-infix {
+// padding: 5px 0 10px 0 !important;
+// .mat-input-element {
+// &::-webkit-input-placeholder {
+// /* Chrome/Opera/Safari */
+// color: var(--color-common-grey);
+// font-weight: normal;
+// }
+// &::-moz-placeholder {
+// /* Firefox 19+ */
+// color: var(--color-common-grey);
+// font-weight: normal;
+// }
+// &:-ms-input-placeholder {
+// /* IE 10+ */
+// color: var(--color-common-grey);
+// font-weight: normal;
+// }
+// &:-moz-placeholder {
+// /* Firefox 18- */
+// color: var(--color-common-grey);
+// font-weight: normal;
+// }
+// }
+// // .mat-form-field-label-wrapper {
+// // .mat-form-field-required-marker {
+// // display: none;
+// // }
+// // }
+// &.mat-form-field-appearance-outline {
+// .mat-form-field-outline {
+// background-color: var(--color-common-white) !important;
+// }
+// }
+// }
+// .mat-form-field-flex {
+// .mat-form-field-outline {
+// .mat-form-field-outline-start {
+// border-radius: var(--border-common-radius-4) 0 0 var(--border-common-radius-4);
+// min-width: var(--border-common-radius-4);
+// }
+// .mat-form-field-outline-end {
+// border-radius: 0 var(--border-common-radius-4) var(--border-common-radius-4) 0;
+// }
+// }
+// }
+// }
+// .mat-form-field-hint-wrapper {
+// .mat-hint {
+// color: var(--color-common-slate);
+// font-size: 12px;
+// line-height: 16px;
+// }
+// }
+// }
+
+// &.no-space {
+// .mat-form-field-wrapper {
+// padding-bottom: 0px !important;
+// }
+// }
+// }
+
+// .mat-form-field-appearance-outline.mat-form-field-can-float.mat-form-field-should-float .mat-form-field-label,
+// .mat-form-field-appearance-outline.mat-form-field-can-float
+// .mat-input-server:focus
+// + .mat-form-field-label-wrapper
+// .mat-form-field-label {
+// transform: translateY(-15px) scale(0.7) !important;
+// }
+
+// .mat-form-field-appearance-outline .mat-form-field-label {
+// top: 18px !important;
+// }
+
+// /* Firefox hide */
+// input[matinput][type='number'] {
+// -moz-appearance: textfield;
+// }
+// /* Chrome, Safari, Edge, Opera */
+// input[matinput]::-webkit-outer-spin-button,
+// input[matinput]::-webkit-inner-spin-button {
+// -webkit-appearance: none;
+// margin: 0;
+// }
+
+// // Material Form Error
+// .mat-error {
+// font-size: 12px;
+// }
+
+// // Used to display only first mat-error
+// mat-error {
+// display: none !important;
+// &:first-child {
+// display: block !important;
+// }
+// }
diff --git a/apps/36-blocks-widget/src/assets/scss/component/_tabs.scss b/apps/36-blocks-widget/src/assets/scss/component/_tabs.scss
new file mode 100644
index 00000000..98889204
--- /dev/null
+++ b/apps/36-blocks-widget/src/assets/scss/component/_tabs.scss
@@ -0,0 +1,24 @@
+// .user-management-tabs {
+// .mat-tab-header {
+// .mat-tab-labels {
+// .mat-tab-label {
+// font-weight: 600 !important;
+// color: var(--color-common-black) !important;
+// opacity: 1 !important;
+// &:focus {
+// color: var(--color-common-black) !important;
+// }
+// }
+// }
+// }
+// }
+
+// .nested-tabs {
+// .mat-tab-header {
+// .mat-tab-labels {
+// .mat-tab-label {
+// font-weight: 500 !important;
+// }
+// }
+// }
+// }
diff --git a/apps/proxy-auth/src/assets/scss/layout/_display.scss b/apps/36-blocks-widget/src/assets/scss/layout/_display.scss
similarity index 100%
rename from apps/proxy-auth/src/assets/scss/layout/_display.scss
rename to apps/36-blocks-widget/src/assets/scss/layout/_display.scss
diff --git a/apps/proxy-auth/src/assets/scss/layout/_spacing.scss b/apps/36-blocks-widget/src/assets/scss/layout/_spacing.scss
similarity index 100%
rename from apps/proxy-auth/src/assets/scss/layout/_spacing.scss
rename to apps/36-blocks-widget/src/assets/scss/layout/_spacing.scss
diff --git a/apps/36-blocks-widget/src/assets/scss/widget-ui.scss b/apps/36-blocks-widget/src/assets/scss/widget-ui.scss
new file mode 100644
index 00000000..2f99dc2a
--- /dev/null
+++ b/apps/36-blocks-widget/src/assets/scss/widget-ui.scss
@@ -0,0 +1,461 @@
+// =============================================================================
+// Widget UI — Shared Utility Classes
+// =============================================================================
+// All classes are prefixed with `w-` (widget) to avoid collisions with
+// Tailwind's built-in utilities and global styles.
+//
+// Usage in templates — replace verbose Tailwind strings with these classes:
+//
+// BEFORE: class="block w-full rounded-lg border border-gray-200 dark:border-gray-600
+// bg-white dark:bg-gray-800 px-3.5 py-2 text-sm text-gray-900
+// dark:text-white placeholder:text-gray-400 dark:placeholder:text-gray-500
+// focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
+// AFTER: class="w-input"
+//
+// =============================================================================
+
+// ── INPUTS ────────────────────────────────────────────────────────────────────
+
+// Standard text / email / tel / number input
+.w-input {
+ @apply block w-full rounded-lg border border-gray-200 bg-white px-3.5 py-2 text-sm
+ text-gray-900 placeholder:text-gray-400
+ focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500;
+
+ .dark & {
+ @apply border-gray-600 bg-gray-800 text-white placeholder:text-gray-500;
+ }
+}
+
+// Input with smaller horizontal padding (px-3 variant used in login / register)
+.w-input-sm {
+ @apply block w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm
+ text-gray-900 placeholder:text-gray-400
+ focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500;
+
+ .dark & {
+ @apply border-gray-600 bg-gray-800 text-white placeholder:text-gray-500;
+ }
+}
+
+// Input with right-side icon padding (password / search-with-icon)
+.w-input-icon-right {
+ @apply block w-full rounded-lg border border-gray-200 bg-white px-3.5 py-2 pr-10 text-sm
+ text-gray-900 placeholder:text-gray-400
+ focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500;
+
+ .dark & {
+ @apply border-gray-600 bg-gray-800 text-white placeholder:text-gray-500;
+ }
+}
+
+// Search input (left-padded for icon, webkit cancel button)
+.w-input-search {
+ @apply block w-full rounded-lg border border-gray-200 bg-white py-2 pl-10 pr-3 text-sm
+ text-gray-900 placeholder:text-gray-400
+ focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500
+ [&::-webkit-search-cancel-button]:cursor-pointer;
+
+ .dark & {
+ @apply border-gray-600 bg-gray-800 text-white placeholder:text-gray-500;
+ }
+}
+
+// Read-only / disabled input (e.g. pre-filled email, mobile)
+.w-input-readonly {
+ @apply block w-full rounded-lg border border-gray-200 bg-white px-3.5 py-2 text-sm
+ text-gray-500 cursor-not-allowed
+ read-only:cursor-default read-only:opacity-60;
+
+ .dark & {
+ @apply border-gray-600 bg-gray-800 text-gray-400;
+ }
+}
+
+// Textarea
+.w-textarea {
+ @apply block w-full rounded-lg border border-gray-200 bg-white px-3.5 py-2 text-sm
+ text-gray-900 placeholder:text-gray-400 resize-none
+ focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500;
+
+ .dark & {
+ @apply border-gray-600 bg-gray-800 text-white placeholder:text-gray-500;
+ }
+}
+
+// Select (always paired with a wrapper div.relative for the custom caret)
+.w-select {
+ @apply block w-full appearance-none rounded-lg border border-gray-200 bg-white
+ pl-3.5 pr-9 py-2 text-sm text-gray-900
+ focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500;
+
+ .dark & {
+ @apply border-gray-600 bg-gray-800 text-white;
+ }
+}
+
+// OTP single-character box
+.w-input-otp {
+ @apply w-9 h-9 text-center text-sm font-medium rounded-lg border border-gray-200
+ bg-white text-gray-900
+ focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500;
+
+ .dark & {
+ @apply border-gray-600 bg-gray-800 text-white;
+ }
+}
+
+// Form field label
+.w-label {
+ @apply block text-sm font-medium text-gray-900 mb-1.5;
+
+ .dark & {
+ @apply text-white;
+ }
+}
+
+// Inline validation error
+.w-field-error {
+ @apply mt-1.5 text-xs text-red-600;
+
+ .dark & {
+ @apply text-red-400;
+ }
+}
+
+// ── BUTTONS ───────────────────────────────────────────────────────────────────
+
+// Primary action button (indigo, md size)
+.w-btn-primary {
+ @apply inline-flex items-center gap-1.5 rounded-lg bg-indigo-600 px-4 py-2 text-sm
+ font-semibold text-white shadow-sm cursor-pointer
+ hover:bg-indigo-500 active:bg-indigo-700 transition-colors duration-150
+ disabled:opacity-50 disabled:cursor-not-allowed
+ focus-visible:outline focus-visible:outline-2 focus-visible:outline-indigo-500;
+}
+
+// Primary action button (indigo, sm / xs size — used in card headers)
+.w-btn-primary-sm {
+ @apply inline-flex items-center gap-1.5 rounded-lg bg-indigo-600 px-3 py-1.5 text-xs
+ font-semibold text-white shadow-sm cursor-pointer
+ hover:bg-indigo-500 active:bg-indigo-700 transition-colors duration-150
+ focus-visible:outline focus-visible:outline-2 focus-visible:outline-indigo-500
+ focus-visible:outline-offset-2;
+}
+
+// Secondary / cancel button (outlined, md size)
+.w-btn-secondary {
+ @apply rounded-lg px-4 py-2 text-sm font-semibold text-gray-700 bg-white
+ ring-1 ring-inset ring-gray-300 cursor-pointer
+ hover:bg-gray-50 transition-colors duration-150
+ disabled:opacity-50 disabled:cursor-not-allowed
+ focus-visible:outline focus-visible:outline-2 focus-visible:outline-indigo-500;
+
+ .dark & {
+ @apply text-gray-300 bg-gray-800 ring-gray-600 hover:bg-gray-700;
+ }
+}
+
+// Secondary / outlined button (sm / xs size — used in card row actions)
+.w-btn-secondary-sm {
+ @apply shrink-0 rounded-md px-3 py-1.5 text-xs font-semibold text-gray-700 bg-white
+ ring-1 ring-inset ring-gray-300 cursor-pointer
+ hover:bg-gray-50 transition-colors duration-150
+ disabled:opacity-40 disabled:cursor-not-allowed
+ focus-visible:outline focus-visible:outline-2 focus-visible:outline-indigo-500;
+
+ .dark & {
+ @apply text-gray-200 bg-gray-800 ring-gray-600 hover:bg-gray-700;
+ }
+}
+
+// Destructive primary button (red, md size — dialog confirm)
+.w-btn-danger {
+ @apply rounded-lg bg-red-600 px-4 py-2 text-sm font-semibold text-white shadow-sm
+ cursor-pointer hover:bg-red-500 active:bg-red-700 transition-colors duration-150
+ focus-visible:outline focus-visible:outline-2 focus-visible:outline-red-500;
+}
+
+// Destructive outlined button (sm — card row remove)
+.w-btn-danger-sm {
+ @apply shrink-0 rounded-md px-3 py-1.5 text-xs font-semibold text-red-600 bg-white
+ ring-1 ring-inset ring-gray-300 cursor-pointer
+ hover:bg-red-50 transition-colors duration-150
+ focus-visible:outline focus-visible:outline-2 focus-visible:outline-red-500;
+
+ .dark & {
+ @apply text-red-400 bg-gray-800 ring-gray-600;
+ &:hover {
+ background-color: rgb(127 29 29 / 0.2);
+ }
+ }
+}
+
+// Icon-only close / dismiss button (dialog header X)
+.w-btn-close {
+ @apply -m-1 p-1 rounded-md text-gray-400 cursor-pointer transition-colors duration-150
+ hover:text-gray-500
+ focus-visible:outline focus-visible:outline-2 focus-visible:outline-indigo-500;
+
+ .dark & {
+ @apply text-gray-500 hover:text-gray-300;
+ }
+}
+
+// Inline spinner (inside buttons while saving)
+.w-spinner {
+ @apply size-4 animate-spin;
+}
+
+// ── CARDS ─────────────────────────────────────────────────────────────────────
+
+// Standard list card (user / permission rows)
+.w-card {
+ @apply rounded-xl border border-gray-200 bg-white;
+
+ .dark & {
+ @apply border-gray-700 bg-gray-900;
+ }
+}
+
+// Section / feature card (with overflow-hidden + shadow, e.g. profile / org cards)
+.w-card-section {
+ @apply rounded-xl border border-gray-200 bg-white overflow-hidden shadow-sm;
+
+ .dark & {
+ @apply border-gray-700 bg-gray-900;
+ }
+}
+
+// ── DIALOGS ───────────────────────────────────────────────────────────────────
+
+// Semi-transparent backdrop
+// z-index: 2147483646 = max-int - 1 to always appear above any client sidebar/overlay
+.w-dialog-backdrop {
+ @apply fixed inset-0 bg-black/50 backdrop-blur-sm;
+ z-index: 2147483646;
+
+ .dark & {
+ @apply bg-black/70;
+ }
+}
+
+// Dialog panel (centered, responsive)
+// z-index: 2147483647 = max-int to always appear above any client sidebar/overlay
+.w-dialog-panel {
+ @apply fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2
+ flex flex-col max-h-[85vh]
+ rounded-xl bg-white shadow-2xl ring-1 ring-gray-900/5;
+ z-index: 2147483647;
+
+ .dark & {
+ @apply bg-gray-900 ring-white/10;
+ }
+}
+
+// Dialog header bar
+.w-dialog-header {
+ @apply flex items-center justify-between px-6 py-4 border-b border-gray-200;
+
+ .dark & {
+ @apply border-gray-700;
+ }
+}
+
+// Dialog title text
+.w-dialog-title {
+ @apply text-base font-semibold text-gray-900;
+
+ .dark & {
+ @apply text-white;
+ }
+}
+
+// Dialog scrollable body
+.w-dialog-body {
+ @apply flex-1 overflow-y-auto min-h-0 px-6 py-5 w-full;
+}
+
+// Forgot-password dialog: hide send-otp-center's own absolute close button + h2 heading
+// (both are replaced by the w-dialog-header rendered in widget.component.html)
+.fp-dialog-content {
+ > authorization,
+ > authorization > send-otp-center {
+ button[aria-label='Close'] {
+ display: none !important;
+ }
+ h2 {
+ display: none !important;
+ }
+ }
+}
+
+// Dialog footer action bar
+.w-dialog-footer {
+ @apply flex items-center justify-end gap-3 px-6 py-4 border-t border-gray-200;
+
+ .dark & {
+ @apply border-gray-700;
+ }
+}
+
+// ── SECTION HEADERS ───────────────────────────────────────────────────────────
+
+// Page/tab section heading
+.w-section-title {
+ @apply text-xl font-bold text-gray-900;
+
+ .dark & {
+ @apply text-white;
+ }
+}
+
+// Page/tab section subtitle
+.w-section-subtitle {
+ @apply mt-1 text-sm text-gray-500;
+
+ .dark & {
+ @apply text-gray-400;
+ }
+}
+
+// ── BADGES / TAGS ─────────────────────────────────────────────────────────────
+
+// Generic neutral badge (role label, "Default" tag)
+.w-badge {
+ @apply inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium
+ bg-gray-100 text-gray-600;
+
+ .dark & {
+ @apply bg-gray-700 text-gray-300;
+ }
+}
+
+// Status badge — green (active/admin)
+.w-badge-green {
+ @apply hidden sm:inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium
+ bg-green-50 text-green-700;
+
+ .dark & {
+ @apply text-green-400;
+ background-color: rgb(20 83 45 / 0.3);
+ }
+}
+
+// ── DIVIDERS ─────────────────────────────────────────────────────────────────
+
+.w-divider {
+ @apply border-t border-gray-100;
+
+ .dark & {
+ @apply border-gray-800;
+ }
+}
+
+// ── AVATAR / ICON CONTAINERS ─────────────────────────────────────────────────
+
+// Round user avatar with initials
+.w-avatar {
+ @apply size-10 flex-none rounded-full bg-indigo-100 flex items-center justify-center
+ text-sm font-semibold text-indigo-700 select-none;
+
+ .dark & {
+ @apply text-indigo-300;
+ background-color: rgb(49 46 129 / 0.6);
+ }
+}
+
+// Square icon container (card / section header icon)
+.w-icon-box {
+ @apply flex size-8 items-center justify-center rounded-lg bg-indigo-100;
+
+ .dark & {
+ background-color: rgb(49 46 129 / 0.5);
+ }
+}
+
+// Icon color inside .w-icon-box
+.w-icon-box svg {
+ @apply text-indigo-600;
+
+ .dark & {
+ @apply text-indigo-400;
+ }
+}
+
+// ── INLINE LINKS ──────────────────────────────────────────────────────────────
+
+// Indigo inline text link (e.g. "Forgot password?", "Resend OTP")
+.w-link {
+ @apply text-xs font-medium text-indigo-600 hover:underline cursor-pointer
+ disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-150;
+
+ .dark & {
+ @apply text-indigo-400;
+ }
+}
+
+// ── NAVIGATION ────────────────────────────────────────────────────────────────
+
+// Horizontal tab button (top tab bar)
+.w-nav-tab {
+ @apply inline-flex shrink-0 items-center gap-2 border-b-2 px-3 py-3
+ text-sm font-medium cursor-pointer whitespace-nowrap transition-colors duration-150;
+}
+
+// Vertical sidebar nav item
+.w-nav-item {
+ @apply flex w-full items-center gap-x-3 rounded-md px-3 py-2
+ text-sm leading-6 text-left cursor-pointer transition-colors duration-150;
+}
+
+// ── CHECKBOX GROUP ────────────────────────────────────────────────────────────
+
+// Scrollable checkbox list container
+.w-checkbox-group {
+ @apply rounded-lg border border-gray-200 bg-gray-50
+ divide-y divide-gray-200 max-h-44 overflow-y-auto;
+
+ .dark & {
+ @apply border-gray-700 bg-gray-800 divide-gray-700;
+ }
+}
+
+// Clickable label row inside a checkbox group
+.w-checkbox-row {
+ @apply flex items-center gap-3 px-3.5 py-2.5 cursor-pointer hover:bg-white;
+
+ .dark & {
+ @apply hover:bg-gray-700;
+ }
+}
+
+// Checkbox input inside a .w-checkbox-row
+.w-checkbox {
+ @apply size-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500;
+
+ .dark & {
+ @apply border-gray-500;
+ }
+}
+
+// ── MISC HELPERS ──────────────────────────────────────────────────────────────
+
+// Pointer-events-none icon inside a search / input wrapper
+.w-search-icon {
+ @apply pointer-events-none absolute inset-y-0 left-3 h-full w-4
+ text-gray-400;
+
+ .dark & {
+ @apply text-gray-500;
+ }
+}
+
+// Micro uppercase label (field meta, e.g. "PHONE", "EMAIL")
+.w-micro-label {
+ @apply text-[10px] font-semibold uppercase tracking-wide text-gray-400 mb-0.5;
+
+ .dark & {
+ @apply text-gray-500;
+ }
+}
diff --git a/apps/proxy-auth/src/environments/environment.prod.ts b/apps/36-blocks-widget/src/environments/environment.prod.ts
similarity index 100%
rename from apps/proxy-auth/src/environments/environment.prod.ts
rename to apps/36-blocks-widget/src/environments/environment.prod.ts
diff --git a/apps/proxy-auth/src/environments/environment.stage.ts b/apps/36-blocks-widget/src/environments/environment.stage.ts
similarity index 100%
rename from apps/proxy-auth/src/environments/environment.stage.ts
rename to apps/36-blocks-widget/src/environments/environment.stage.ts
diff --git a/apps/proxy-auth/src/environments/environment.test.ts b/apps/36-blocks-widget/src/environments/environment.test.ts
similarity index 100%
rename from apps/proxy-auth/src/environments/environment.test.ts
rename to apps/36-blocks-widget/src/environments/environment.test.ts
diff --git a/apps/proxy-auth/src/environments/environment.ts b/apps/36-blocks-widget/src/environments/environment.ts
similarity index 83%
rename from apps/proxy-auth/src/environments/environment.ts
rename to apps/36-blocks-widget/src/environments/environment.ts
index fdc600da..57b39a62 100644
--- a/apps/proxy-auth/src/environments/environment.ts
+++ b/apps/36-blocks-widget/src/environments/environment.ts
@@ -11,6 +11,8 @@ export const environment = {
baseUrl: 'https://test.proxy.msg91.com',
msgMidProxy: '',
...envVariables,
+ // hCaptcha official test sitekey — whitelisted for localhost, suppresses the "localhost detected" warning
+ hCaptchaSiteKey: '10000000-ffff-ffff-ffff-000000000001',
};
/*
diff --git a/apps/proxy-auth-element/src/favicon.ico b/apps/36-blocks-widget/src/favicon.ico
similarity index 100%
rename from apps/proxy-auth-element/src/favicon.ico
rename to apps/36-blocks-widget/src/favicon.ico
diff --git a/apps/proxy-auth/src/index.html b/apps/36-blocks-widget/src/index.html
similarity index 94%
rename from apps/proxy-auth/src/index.html
rename to apps/36-blocks-widget/src/index.html
index 738252f8..facdee80 100644
--- a/apps/proxy-auth/src/index.html
+++ b/apps/36-blocks-widget/src/index.html
@@ -2,7 +2,7 @@
- OtpProvider
+ 36 Blocks Widget
diff --git a/apps/36-blocks-widget/src/main.dev.ts b/apps/36-blocks-widget/src/main.dev.ts
new file mode 100644
index 00000000..24039cc2
--- /dev/null
+++ b/apps/36-blocks-widget/src/main.dev.ts
@@ -0,0 +1,60 @@
+import 'zone.js';
+import { bootstrapApplication } from '@angular/platform-browser';
+import { createCustomElement } from '@angular/elements';
+import { provideHttpClient } from '@angular/common/http';
+import { provideAnimations } from '@angular/platform-browser/animations';
+import { provideStore } from '@ngrx/store';
+import { provideEffects } from '@ngrx/effects';
+import { provideStoreDevtools } from '@ngrx/store-devtools';
+import { provideZoneChangeDetection, importProvidersFrom, Injector } from '@angular/core';
+import { NgHcaptchaModule } from 'ng-hcaptcha';
+import { AppComponent } from './app/app.component';
+import { ProxyAuthWidgetComponent } from './app/otp/widget/widget.component';
+import { reducers } from './app/otp/store/app.state';
+import { OtpEffects } from './app/otp/store/effects';
+import { OtpService } from './app/otp/service/otp.service';
+import { OtpUtilityService } from './app/otp/service/otp-utility.service';
+import { OtpWidgetService } from './app/otp/service/otp-widget.service';
+import { WidgetThemeService } from './app/otp/service/widget-theme.service';
+import { ProxyBaseUrls } from '@proxy/models/root-models';
+import { environment } from './environments/environment';
+
+import './app/init-verification';
+
+bootstrapApplication(AppComponent, {
+ providers: [
+ provideZoneChangeDetection({ eventCoalescing: true }),
+ provideHttpClient(),
+ provideAnimations(),
+ provideStore(reducers, {
+ runtimeChecks: {
+ strictStateImmutability: true,
+ strictActionImmutability: true,
+ },
+ }),
+ provideEffects([OtpEffects]),
+ provideStoreDevtools({ maxAge: 25, serialize: true }),
+ importProvidersFrom(NgHcaptchaModule.forRoot({ siteKey: environment.hCaptchaSiteKey })),
+ OtpService,
+ OtpUtilityService,
+ OtpWidgetService,
+ WidgetThemeService,
+ { provide: ProxyBaseUrls.Env, useValue: environment.env },
+ {
+ provide: ProxyBaseUrls.BaseURL,
+ useValue: environment.apiUrl + environment.msgMidProxy,
+ },
+ {
+ provide: ProxyBaseUrls.ClientURL,
+ useValue: environment.apiUrl + environment.msgMidProxy,
+ },
+ ],
+})
+ .then((appRef) => {
+ const injector = appRef.injector.get(Injector);
+ if (!customElements.get('proxy-auth')) {
+ const el = createCustomElement(ProxyAuthWidgetComponent, { injector });
+ customElements.define('proxy-auth', el);
+ }
+ })
+ .catch((err) => console.error(err));
diff --git a/apps/36-blocks-widget/src/main.ts b/apps/36-blocks-widget/src/main.ts
new file mode 100644
index 00000000..e35c5a78
--- /dev/null
+++ b/apps/36-blocks-widget/src/main.ts
@@ -0,0 +1,70 @@
+import 'zone.js';
+import { createApplication } from '@angular/platform-browser';
+import { createCustomElement } from '@angular/elements';
+import { provideHttpClient } from '@angular/common/http';
+import { provideAnimations } from '@angular/platform-browser/animations';
+import { provideStore } from '@ngrx/store';
+import { provideEffects } from '@ngrx/effects';
+import { provideStoreDevtools } from '@ngrx/store-devtools';
+import { provideZoneChangeDetection, importProvidersFrom } from '@angular/core';
+import { NgHcaptchaModule } from 'ng-hcaptcha';
+import { ProxyAuthWidgetComponent } from './app/otp/widget/widget.component';
+import { reducers } from './app/otp/store/app.state';
+import { OtpEffects } from './app/otp/store/effects';
+import { OtpService } from './app/otp/service/otp.service';
+import { OtpUtilityService } from './app/otp/service/otp-utility.service';
+import { OtpWidgetService } from './app/otp/service/otp-widget.service';
+import { WidgetThemeService } from './app/otp/service/widget-theme.service';
+import { ProxyBaseUrls } from '@proxy/models/root-models';
+import { environment } from './environments/environment';
+
+// Side-effect import — registers window.initVerification, showUserManagement, hideUserManagement
+import './app/init-verification';
+
+// Double-load protection — prevents duplicate Angular app if script is loaded twice
+if ((window as any).__proxyAuthLoaded) {
+ console.warn('[proxy-auth] Script already loaded — skipping bootstrap.');
+} else {
+ (window as any).__proxyAuthLoaded = true;
+
+ createApplication({
+ providers: [
+ provideZoneChangeDetection({ eventCoalescing: true }),
+ provideHttpClient(),
+ provideAnimations(),
+ provideStore(reducers, {
+ runtimeChecks: {
+ strictStateImmutability: true,
+ strictActionImmutability: true,
+ },
+ }),
+ provideEffects([OtpEffects]),
+ ...(!environment.production ? [provideStoreDevtools({ maxAge: 25, serialize: true })] : []),
+ // ng-hcaptcha may not expose standalone providers — use importProvidersFrom as safe fallback
+ importProvidersFrom(NgHcaptchaModule.forRoot({ siteKey: environment.hCaptchaSiteKey })),
+ OtpService,
+ OtpUtilityService,
+ OtpWidgetService,
+ WidgetThemeService,
+ { provide: ProxyBaseUrls.Env, useValue: environment.env },
+ {
+ provide: ProxyBaseUrls.BaseURL,
+ useValue: environment.apiUrl + environment.msgMidProxy,
+ },
+ {
+ provide: ProxyBaseUrls.ClientURL,
+ useValue: environment.apiUrl + environment.msgMidProxy,
+ },
+ ],
+ }).then((appRef) => {
+ const injector = appRef.injector;
+ try {
+ if (!customElements.get('proxy-auth')) {
+ const el = createCustomElement(ProxyAuthWidgetComponent, { injector });
+ customElements.define('proxy-auth', el);
+ }
+ } catch (e) {
+ console.warn('[proxy-auth] Custom element registration failed:', e);
+ }
+ });
+}
diff --git a/apps/36-blocks-widget/src/otp-global.scss b/apps/36-blocks-widget/src/otp-global.scss
new file mode 100644
index 00000000..160e6605
--- /dev/null
+++ b/apps/36-blocks-widget/src/otp-global.scss
@@ -0,0 +1,29 @@
+// .otp-verification-dialog {
+// width: 380px;
+// min-height: 350px;
+// font-size: 15px;
+// text-align: center;
+// display: inherit;
+// flex-direction: column;
+// align-items: inherit;
+// box-shadow: 0 11px 15px -7px #0003, 0 24px 38px 3px #00000024, 0 9px 46px 8px #0000001f;
+// background: transparent;
+// color: #000000de;
+// z-index: 999999 !important;
+// border-radius: 10px;
+// padding: 25px 32px;
+// justify-content: center;
+// position: relative;
+// overflow-y: auto;
+// @media only screen and (max-width: 768px) {
+// width: 90% !important;
+// height: 80%;
+// display: flex;
+// align-items: center;
+// justify-content: center;
+// }
+// &.dark-theme {
+// background: transparent;
+// color: #ffffff !important;
+// }
+// }
diff --git a/apps/proxy-auth/src/polyfills.element.ts b/apps/36-blocks-widget/src/polyfills.element.ts
similarity index 100%
rename from apps/proxy-auth/src/polyfills.element.ts
rename to apps/36-blocks-widget/src/polyfills.element.ts
diff --git a/apps/36-blocks-widget/src/styles.scss b/apps/36-blocks-widget/src/styles.scss
new file mode 100644
index 00000000..297ec805
--- /dev/null
+++ b/apps/36-blocks-widget/src/styles.scss
@@ -0,0 +1,69 @@
+/* Layout*/
+@use 'assets/scss/layout/spacing';
+@use 'assets/scss/layout/display';
+
+/* Components*/
+@use 'assets/scss/component/form-field';
+@use 'assets/scss/component/tabs';
+@use '../../shared/scss/global';
+
+/* Widget shared UI utilities */
+@use 'assets/scss/widget-ui';
+
+/* intl-tel-input custom overrides — global so dropdown applies outside Shadow DOM */
+@use '../../shared/assets/utils/intl-tel-input-custom';
+
+:root {
+ color-scheme: light dark;
+ --color-common-dark: #030712;
+ --color-common-slate: #333333;
+ --color-common-rock: #5d6164;
+ --color-common-grey: #333333;
+ --color-common-cloud: #c1c5c8;
+ --color-common-smoke: #d5d9dc;
+ --color-common-white: #ffffff;
+ --color-common-black: #000000;
+ --border-common-radius-4: 4px;
+ --font-size-12: 12px;
+ --font-size-14: 14px;
+ --font-size-16: 16px;
+ --font-size-18: 18px;
+ --font-size-24: 24px;
+ --font-size-28: 28px;
+ --font-size-30: 30px;
+ --font-size-36: 36px;
+
+ --custom-mat-form-field-height: 48px;
+}
+
+/* You can add global styles to this file, and also import other style files */
+html,
+body {
+ margin: 0;
+ width: 100vw;
+ height: 100vh;
+ overflow-x: hidden;
+ &.light-theme {
+ background-color: var(--color-common-white) !important;
+ color: var(--color-common-dark);
+ }
+ &.dark-theme {
+ background-color: var(--color-common-dark) !important;
+ color: var(--color-common-white);
+ }
+ &.system-theme {
+ background-color: light-dark(var(--color-common-white), var(--color-common-dark)) !important;
+ color: light-dark(var(--color-common-dark), var(--color-common-white));
+ }
+}
+
+*,
+proxy-auth,
+.iti__country-list {
+ font-family: 'Inter', sans-serif;
+ -webkit-font-smoothing: antialiased;
+}
+
+* {
+ box-sizing: border-box;
+}
diff --git a/apps/36-blocks-widget/tests/contract.spec.ts b/apps/36-blocks-widget/tests/contract.spec.ts
new file mode 100644
index 00000000..20a52bb1
--- /dev/null
+++ b/apps/36-blocks-widget/tests/contract.spec.ts
@@ -0,0 +1,82 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+/**
+ * Widget Public API Contract Tests
+ *
+ * These tests verify that the widget's public API contract remains stable:
+ * - window.initVerification is a function
+ * - custom element is registered
+ * - Error handling for missing referenceId and success callback
+ * - DOM rendering inside provided container
+ *
+ * Run after every widget build: npm run test:contract
+ */
+
+export {}; // make this file a module so the declare global works
+
+declare global {
+ interface Window {
+ initVerification: any;
+ __proxyAuth: any;
+ __proxyAuthLoaded: boolean;
+ }
+}
+
+describe('Widget Public API Contract', () => {
+ it('should register window.initVerification as a function', () => {
+ expect(window.initVerification).toBeDefined();
+ expect(typeof window.initVerification).toBe('function');
+ });
+
+ it('should register custom element', () => {
+ expect(customElements.get('proxy-auth')).toBeDefined();
+ });
+
+ it('should expose version metadata on window.__proxyAuth', () => {
+ expect(window.__proxyAuth).toBeDefined();
+ expect(window.__proxyAuth.version).toBeDefined();
+ expect(typeof window.__proxyAuth.version).toBe('string');
+ expect(window.__proxyAuth.buildTime).toBeDefined();
+ });
+
+ it('should throw if referenceId is missing', () => {
+ expect(() => window.initVerification({})).toThrow('Reference Id is missing!');
+ });
+
+ it('should throw if success callback is missing', () => {
+ const div = document.createElement('div');
+ div.id = 'test-ref';
+ document.body.appendChild(div);
+ expect(() => window.initVerification({ referenceId: 'test-ref' })).toThrow('success callback function missing');
+ div.remove();
+ });
+
+ it('should render inside provided container', async () => {
+ const div = document.createElement('div');
+ div.id = 'render-test';
+ document.body.appendChild(div);
+ window.initVerification({
+ referenceId: 'render-test',
+ success: () => {},
+ });
+
+ // Stable polling — avoids flaky setTimeout in CI
+ const waitForElement = (container: Element, selector: string, timeout = 2000) =>
+ new Promise((resolve, reject) => {
+ const start = Date.now();
+ const interval = setInterval(() => {
+ const el = container.querySelector(selector);
+ if (el) {
+ clearInterval(interval);
+ resolve(el);
+ } else if (Date.now() - start > timeout) {
+ clearInterval(interval);
+ reject(new Error(`${selector} not found within ${timeout}ms`));
+ }
+ }, 20);
+ });
+
+ const el = await waitForElement(div, 'proxy-auth');
+ expect(el).not.toBeNull();
+ div.remove();
+ });
+});
diff --git a/apps/proxy-auth-element/tsconfig.app.json b/apps/36-blocks-widget/tsconfig.app.json
similarity index 73%
rename from apps/proxy-auth-element/tsconfig.app.json
rename to apps/36-blocks-widget/tsconfig.app.json
index 18bf7d57..71b9a4d0 100644
--- a/apps/proxy-auth-element/tsconfig.app.json
+++ b/apps/36-blocks-widget/tsconfig.app.json
@@ -2,9 +2,9 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
- "types": []
+ "types": ["node"]
},
- "files": ["src/main.ts", "src/polyfills.ts"],
+ "files": ["src/main.ts", "src/main.dev.ts"],
"include": ["src/**/*.d.ts"],
"exclude": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts"]
}
diff --git a/apps/proxy-auth-element/tsconfig.editor.json b/apps/36-blocks-widget/tsconfig.editor.json
similarity index 100%
rename from apps/proxy-auth-element/tsconfig.editor.json
rename to apps/36-blocks-widget/tsconfig.editor.json
diff --git a/apps/proxy-auth/tsconfig.json b/apps/36-blocks-widget/tsconfig.json
similarity index 78%
rename from apps/proxy-auth/tsconfig.json
rename to apps/36-blocks-widget/tsconfig.json
index 41e85e79..18bea8e2 100644
--- a/apps/proxy-auth/tsconfig.json
+++ b/apps/36-blocks-widget/tsconfig.json
@@ -8,6 +8,9 @@
},
{
"path": "./tsconfig.editor.json"
+ },
+ {
+ "path": "./tsconfig.spec.json"
}
]
}
diff --git a/libs/shared/tsconfig.spec.json b/apps/36-blocks-widget/tsconfig.spec.json
similarity index 63%
rename from libs/shared/tsconfig.spec.json
rename to apps/36-blocks-widget/tsconfig.spec.json
index 59ee35a7..04701c8c 100644
--- a/libs/shared/tsconfig.spec.json
+++ b/apps/36-blocks-widget/tsconfig.spec.json
@@ -5,5 +5,6 @@
"module": "commonjs",
"types": ["jest", "node"]
},
- "include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts", "jest.config.ts"]
+ "files": ["tests/contract.spec.ts"],
+ "include": ["tests/**/*.spec.ts", "tests/**/*.d.ts"]
}
diff --git a/apps/proxy-auth/webpack.config.js b/apps/36-blocks-widget/webpack.config.js
similarity index 100%
rename from apps/proxy-auth/webpack.config.js
rename to apps/36-blocks-widget/webpack.config.js
diff --git a/apps/proxy-auth/.eslintrc.json b/apps/36-blocks/.eslintrc.json
similarity index 100%
rename from apps/proxy-auth/.eslintrc.json
rename to apps/36-blocks/.eslintrc.json
diff --git a/apps/proxy/project.json b/apps/36-blocks/project.json
similarity index 58%
rename from apps/proxy/project.json
rename to apps/36-blocks/project.json
index b4875c74..736a734b 100644
--- a/apps/proxy/project.json
+++ b/apps/36-blocks/project.json
@@ -1,27 +1,41 @@
{
- "name": "proxy",
+ "name": "36-blocks",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
- "sourceRoot": "apps/proxy/src",
+ "sourceRoot": "apps/36-blocks/src",
"prefix": "proxy",
"targets": {
+ "set-env": {
+ "executor": "nx:run-commands",
+ "options": {
+ "command": "node tools/set-env --proxy"
+ }
+ },
"build": {
- "executor": "@angular-builders/custom-webpack:browser",
+ "dependsOn": ["set-env"],
+ "executor": "@angular-devkit/build-angular:application",
"outputs": ["{options.outputPath}"],
"options": {
- "outputPath": "dist/apps/proxy",
- "index": "apps/proxy/src/index.html",
- "main": "apps/proxy/src/main.ts",
- "polyfills": "apps/proxy/src/polyfills.ts",
- "tsConfig": "apps/proxy/tsconfig.app.json",
+ "outputPath": "dist/apps/36-blocks",
+ "index": "apps/36-blocks/src/index.html",
+ "browser": "apps/36-blocks/src/main.ts",
+ "polyfills": ["zone.js"],
+ "tsConfig": "apps/36-blocks/tsconfig.app.json",
"inlineStyleLanguage": "scss",
- "customWebpackConfig": {
- "path": "/webpack.config.js"
+ "stylePreprocessorOptions": {
+ "includePaths": ["apps/shared/scss"]
},
- "assets": ["apps/proxy/src/favicon.ico", "apps/proxy/src/assets"],
+ "assets": [
+ "apps/36-blocks/src/favicon.ico",
+ "apps/36-blocks/src/assets",
+ {
+ "glob": "intl-tel-input-custom.css",
+ "input": "apps/shared/assets/utils",
+ "output": "assets/utils"
+ }
+ ],
"styles": [
- "apps/proxy/src/styles.scss",
- "node_modules/primeng/resources/themes/md-light-indigo/theme.css",
+ "apps/36-blocks/src/styles.scss",
"node_modules/primeng/resources/primeng.min.css",
"node_modules/prismjs/themes/prism-okaidia.css"
],
@@ -29,7 +43,8 @@
"node_modules/prismjs/prism.js",
"node_modules/prismjs/components/prism-csharp.min.js",
"node_modules/prismjs/components/prism-css.min.js"
- ]
+ ],
+ "allowedCommonJsDependencies": ["crypto-js", "dayjs", "prismjs"]
},
"configurations": {
"production": {
@@ -47,8 +62,8 @@
],
"fileReplacements": [
{
- "replace": "apps/proxy/src/environments/environment.ts",
- "with": "apps/proxy/src/environments/environment.prod.ts"
+ "replace": "apps/36-blocks/src/environments/environment.ts",
+ "with": "apps/36-blocks/src/environments/environment.prod.ts"
}
],
"outputHashing": "all"
@@ -68,8 +83,8 @@
],
"fileReplacements": [
{
- "replace": "apps/proxy/src/environments/environment.ts",
- "with": "apps/proxy/src/environments/environment.stage.ts"
+ "replace": "apps/36-blocks/src/environments/environment.ts",
+ "with": "apps/36-blocks/src/environments/environment.stage.ts"
}
],
"outputHashing": "all"
@@ -89,31 +104,37 @@
],
"fileReplacements": [
{
- "replace": "apps/proxy/src/environments/environment.ts",
- "with": "apps/proxy/src/environments/environment.test.ts"
+ "replace": "apps/36-blocks/src/environments/environment.ts",
+ "with": "apps/36-blocks/src/environments/environment.test.ts"
}
],
"outputHashing": "all"
},
"development": {
- "buildOptimizer": false,
"optimization": false,
- "vendorChunk": true,
"extractLicenses": false,
- "sourceMap": true,
- "namedChunks": true
+ "sourceMap": true
+ },
+ "local": {
+ "optimization": false,
+ "extractLicenses": false,
+ "sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
- "executor": "@angular-builders/custom-webpack:dev-server",
+ "dependsOn": ["set-env"],
+ "executor": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
- "browserTarget": "proxy:build:production"
+ "buildTarget": "36-blocks:build:production"
},
"development": {
- "browserTarget": "proxy:build:development"
+ "buildTarget": "36-blocks:build:development"
+ },
+ "serve": {
+ "buildTarget": "36-blocks:build:development"
}
},
"defaultConfiguration": "development"
@@ -121,13 +142,13 @@
"extract-i18n": {
"executor": "@angular-devkit/build-angular:extract-i18n",
"options": {
- "browserTarget": "proxy:build"
+ "buildTarget": "36-blocks:build"
}
},
"lint": {
- "executor": "@nrwl/linter:eslint",
+ "executor": "@nx/eslint:lint",
"options": {
- "lintFilePatterns": ["apps/proxy/**/*.ts", "apps/proxy/**/*.html"]
+ "lintFilePatterns": ["apps/36-blocks/**/*.ts", "apps/36-blocks/**/*.html"]
}
}
},
diff --git a/apps/proxy/src/app/app.component.html b/apps/36-blocks/src/app/app.component.html
similarity index 100%
rename from apps/proxy/src/app/app.component.html
rename to apps/36-blocks/src/app/app.component.html
diff --git a/apps/proxy/src/app/app.component.ts b/apps/36-blocks/src/app/app.component.ts
similarity index 85%
rename from apps/proxy/src/app/app.component.ts
rename to apps/36-blocks/src/app/app.component.ts
index dd918e50..4447f220 100644
--- a/apps/proxy/src/app/app.component.ts
+++ b/apps/36-blocks/src/app/app.component.ts
@@ -1,4 +1,6 @@
-import { Component, OnDestroy, OnInit } from '@angular/core';
+import { ChangeDetectionStrategy, Component, OnDestroy, OnInit, inject } from '@angular/core';
+import { RouterModule } from '@angular/router';
+import { PrimeNgToastComponent } from '@proxy/ui/prime-ng-toast';
import { ActivatedRoute, NavigationEnd, NavigationStart, Router } from '@angular/router';
import { VersionCheckService } from '@proxy/service';
import { select, Store } from '@ngrx/store';
@@ -15,7 +17,9 @@ import * as logInActions from './auth/ngrx/actions/login.action';
import { IClientSettings, IFirebaseUserModel } from '@proxy/models/root-models';
@Component({
+ changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'proxy-root',
+ imports: [RouterModule, PrimeNgToastComponent],
templateUrl: './app.component.html',
})
export class AppComponent extends BaseComponent implements OnInit, OnDestroy {
@@ -28,13 +32,13 @@ export class AppComponent extends BaseComponent implements OnInit, OnDestroy {
/** True, if new build is deployed */
private newVersionAvailableForWebApp: boolean = false;
- constructor(
- private _store: Store,
- private router: Router,
- private actRoute: ActivatedRoute,
- private store: Store,
- private versionCheckService: VersionCheckService
- ) {
+ private _store = inject>(Store);
+ private router = inject(Router);
+ private actRoute = inject(ActivatedRoute);
+ private store = inject>(Store);
+ private versionCheckService = inject(VersionCheckService);
+
+ constructor() {
super();
this._store.dispatch(logInActions.getUserAction());
diff --git a/apps/proxy/src/app/app.routes.ts b/apps/36-blocks/src/app/app.routes.ts
similarity index 60%
rename from apps/proxy/src/app/app.routes.ts
rename to apps/36-blocks/src/app/app.routes.ts
index 8bae04c5..ca261795 100644
--- a/apps/proxy/src/app/app.routes.ts
+++ b/apps/36-blocks/src/app/app.routes.ts
@@ -7,26 +7,26 @@ export const appRoutes: Route[] = [
{ path: '', redirectTo: 'login', pathMatch: 'full' },
{
path: 'login',
- loadChildren: () => import('./auth/auth.module').then((p) => p.AuthModule),
+ loadComponent: () => import('./auth/auth.component').then((c) => c.AuthComponent),
},
{
path: 'app',
- loadChildren: () => import('./layout/layout.module').then((p) => p.LayoutModule),
+ loadChildren: () => import('./layout/layout.routes').then((r) => r.layoutRoutes),
data: { authGuardPipe: redirectUnauthorizedToLogin },
canActivate: [AngularFireAuthGuard],
},
{
path: 'project',
- loadChildren: () => import('../app/create-project/create-project.module').then((p) => p.CreateProjectModule),
+ loadComponent: () => import('./create-project/create-project.component').then((c) => c.CreateProjectComponent),
data: { authGuardPipe: redirectUnauthorizedToLogin },
canActivate: [AngularFireAuthGuard],
},
{
path: 'p',
- loadChildren: () => import('./public.module').then((p) => p.PublicModule),
+ loadChildren: () => import('./public.routes').then((r) => r.publicRoutes),
},
{
path: 'client',
- loadChildren: () => import('./client.module').then((p) => p.ClientModule),
+ loadChildren: () => import('./client.routes').then((r) => r.clientRoutes),
},
];
diff --git a/apps/proxy/src/app/auth/auth.component.html b/apps/36-blocks/src/app/auth/auth.component.html
similarity index 100%
rename from apps/proxy/src/app/auth/auth.component.html
rename to apps/36-blocks/src/app/auth/auth.component.html
diff --git a/apps/proxy/src/app/auth/auth.component.scss b/apps/36-blocks/src/app/auth/auth.component.scss
similarity index 99%
rename from apps/proxy/src/app/auth/auth.component.scss
rename to apps/36-blocks/src/app/auth/auth.component.scss
index c11a35c2..0d58ef2d 100644
--- a/apps/proxy/src/app/auth/auth.component.scss
+++ b/apps/36-blocks/src/app/auth/auth.component.scss
@@ -1,5 +1,5 @@
+@use '../../../../shared/scss/mixins/common-utils' as *;
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap');
-@import '../../assets/scss/utils/mixins/common-utils';
$bg: #0d1117;
$bg-secondary: #0f1419;
diff --git a/apps/proxy/src/app/auth/auth.component.ts b/apps/36-blocks/src/app/auth/auth.component.ts
similarity index 82%
rename from apps/proxy/src/app/auth/auth.component.ts
rename to apps/36-blocks/src/app/auth/auth.component.ts
index c2387749..04c50284 100644
--- a/apps/proxy/src/app/auth/auth.component.ts
+++ b/apps/36-blocks/src/app/auth/auth.component.ts
@@ -1,10 +1,15 @@
-import { Component, OnInit } from '@angular/core';
+import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit, inject } from '@angular/core';
+import { RouterModule } from '@angular/router';
+import { ReactiveFormsModule } from '@angular/forms';
+import { MatCardModule } from '@angular/material/card';
+import { MatButtonModule } from '@angular/material/button';
+import { MatIconModule } from '@angular/material/icon';
import { Router } from '@angular/router';
import { IFirebaseUserModel } from '@proxy/models/root-models';
import { BaseComponent } from '@proxy/ui/base-component';
import { PrimeNgToastService } from '@proxy/ui/prime-ng-toast';
import { Store, select } from '@ngrx/store';
-import { isEqual } from 'lodash';
+import { isEqual } from 'lodash-es';
import { Observable, distinctUntilChanged, takeUntil, debounceTime } from 'rxjs';
import {
selectLogInErrors,
@@ -16,7 +21,9 @@ import { ILogInFeatureStateWithRootState } from './ngrx/store/login.state';
import * as logInActions from './ngrx/actions/login.action';
@Component({
+ changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'proxy-auth',
+ imports: [RouterModule, ReactiveFormsModule, MatCardModule, MatButtonModule, MatIconModule],
templateUrl: './auth.component.html',
styleUrls: ['./auth.component.scss'],
})
@@ -26,6 +33,11 @@ export class AuthComponent extends BaseComponent implements OnInit {
public logInDataInProcess$: Observable;
public logInDataSuccess$: Observable;
+ private toast = inject(PrimeNgToastService);
+ private _store = inject>(Store);
+ private router = inject(Router);
+ private cdr = inject(ChangeDetectorRef);
+
public currentlyBuilding: string = '';
private buildingTexts = [
'A developer-friendly authentication platform with social login, OTP, analytics, and role-based access control.',
@@ -35,11 +47,7 @@ export class AuthComponent extends BaseComponent implements OnInit {
private isDeleting = false;
private typewriterInterval: any;
- constructor(
- private toast: PrimeNgToastService,
- private _store: Store,
- private router: Router
- ) {
+ constructor() {
super();
this.selectLogInErrors$ = this._store.pipe(
@@ -110,6 +118,7 @@ export class AuthComponent extends BaseComponent implements OnInit {
this.currentTextIndex = (this.currentTextIndex + 1) % this.buildingTexts.length;
}
}
+ this.cdr.markForCheck();
},
this.isDeleting ? 50 : 100
);
diff --git a/apps/proxy/src/app/auth/authguard/index.ts b/apps/36-blocks/src/app/auth/authguard/index.ts
similarity index 81%
rename from apps/proxy/src/app/auth/authguard/index.ts
rename to apps/36-blocks/src/app/auth/authguard/index.ts
index ad0367e6..dc80d9f8 100755
--- a/apps/proxy/src/app/auth/authguard/index.ts
+++ b/apps/36-blocks/src/app/auth/authguard/index.ts
@@ -1,11 +1,11 @@
import { Injectable } from '@angular/core';
-import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
+import { ActivatedRouteSnapshot, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
import { AuthService } from '@proxy/services/proxy/auth';
import { CookieService } from 'ngx-cookie-service';
import { Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
-export class CanActivateRouteGuard implements CanActivate {
+export class CanActivateRouteGuard {
constructor(private cookieService: CookieService, private authService: AuthService, private router: Router) {}
canActivate(
diff --git a/apps/proxy/src/app/auth/ngrx/actions/index.ts b/apps/36-blocks/src/app/auth/ngrx/actions/index.ts
similarity index 100%
rename from apps/proxy/src/app/auth/ngrx/actions/index.ts
rename to apps/36-blocks/src/app/auth/ngrx/actions/index.ts
diff --git a/apps/proxy/src/app/auth/ngrx/actions/login.action.ts b/apps/36-blocks/src/app/auth/ngrx/actions/login.action.ts
similarity index 100%
rename from apps/proxy/src/app/auth/ngrx/actions/login.action.ts
rename to apps/36-blocks/src/app/auth/ngrx/actions/login.action.ts
diff --git a/apps/proxy/src/app/auth/ngrx/effects/login.effects.ts b/apps/36-blocks/src/app/auth/ngrx/effects/login.effects.ts
similarity index 100%
rename from apps/proxy/src/app/auth/ngrx/effects/login.effects.ts
rename to apps/36-blocks/src/app/auth/ngrx/effects/login.effects.ts
diff --git a/apps/proxy/src/app/auth/ngrx/selector/login.selector.ts b/apps/36-blocks/src/app/auth/ngrx/selector/login.selector.ts
similarity index 100%
rename from apps/proxy/src/app/auth/ngrx/selector/login.selector.ts
rename to apps/36-blocks/src/app/auth/ngrx/selector/login.selector.ts
diff --git a/apps/proxy/src/app/auth/ngrx/store/login.reducer.ts b/apps/36-blocks/src/app/auth/ngrx/store/login.reducer.ts
similarity index 100%
rename from apps/proxy/src/app/auth/ngrx/store/login.reducer.ts
rename to apps/36-blocks/src/app/auth/ngrx/store/login.reducer.ts
diff --git a/apps/proxy/src/app/auth/ngrx/store/login.state.ts b/apps/36-blocks/src/app/auth/ngrx/store/login.state.ts
similarity index 100%
rename from apps/proxy/src/app/auth/ngrx/store/login.state.ts
rename to apps/36-blocks/src/app/auth/ngrx/store/login.state.ts
diff --git a/apps/36-blocks/src/app/chatbot/chatbot.component.ts b/apps/36-blocks/src/app/chatbot/chatbot.component.ts
new file mode 100644
index 00000000..0437ab0a
--- /dev/null
+++ b/apps/36-blocks/src/app/chatbot/chatbot.component.ts
@@ -0,0 +1,9 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+
+@Component({
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ selector: 'app-chatbot',
+ imports: [],
+ template: `
`,
+})
+export class ChatbotComponent {}
diff --git a/apps/36-blocks/src/app/client.routes.ts b/apps/36-blocks/src/app/client.routes.ts
new file mode 100644
index 00000000..00927f5b
--- /dev/null
+++ b/apps/36-blocks/src/app/client.routes.ts
@@ -0,0 +1,8 @@
+import { Route } from '@angular/router';
+
+export const clientRoutes: Route[] = [
+ {
+ path: 'registration',
+ loadComponent: () => import('./registration/registration.component').then((c) => c.RegistrationComponent),
+ },
+];
diff --git a/apps/proxy/src/app/create-project/create-project.component.html b/apps/36-blocks/src/app/create-project/create-project.component.html
similarity index 53%
rename from apps/proxy/src/app/create-project/create-project.component.html
rename to apps/36-blocks/src/app/create-project/create-project.component.html
index 4365d980..b5099f6c 100644
--- a/apps/proxy/src/app/create-project/create-project.component.html
+++ b/apps/36-blocks/src/app/create-project/create-project.component.html
@@ -1,31 +1,30 @@
-
-
+
+
+ @if (currentstep === 1) {
-
Create Project
+ Create Project
-
+
Next
navigate_next
-
+ } @if (currentstep == 2) {
-
Add Gateway URL
+
Add Gateway URL
The Gateway URL is the entry point through which incoming requests are received and forwarded to the
Proxy Server for processing
@@ -33,41 +32,41 @@
Add Gateway URL
-
+
Next
navigate_next
-
+ } @if (currentstep == 3) {
-
Add Destination Url
+
Add Destination Url
The destination URL refers to the targeted URL or endpoint where the request will be forwarded by the
proxy Server.
-
-
+
+
navigate_before Back
- Submit
+ Submit
+ }
-
+
Add Destination Url
"
>
-
+
-
+ @for (environment of (environments$ | async)?.data; track environment.name) {
+
{{ environment.name }}
+ }
-
-
- Environment is required
-
-
+ @if (primaryDetailsForm.get('selectedEnvironments').touched &&
+ primaryDetailsForm.get('selectedEnvironments')?.errors?.required) {
+ Environment is required
+ }
-
+
Add Destination Url
"
>
-
Hits per
+
Hits per
Add Destination Url
-
+
-
Provide your own
+
Provide your own
Same for all environments
-
-
-
-
-
-
-
-
-
+ }
+ "
+ >
+ } @else { @for (control of gatewayUrls.controls; track i; let i = $index) {
+
+ } }
-
-
-
OR
-
+
+
+
OR
+
-
Use Ours
+
Use Ours
-
-
{{ slug.name }}
-
{{ slug.url }}
+ @for (slug of environments_with_slug; track slug.name) {
+
-
-
Note:
+ }
+
+ Note:
please copy the provided Gateway URL and utilize it within your user interface to invoke the API by
specifying the desired endpoint for seamless request forwarding to the Proxy Server
@@ -205,9 +199,10 @@
{{ slug.name }}
-
-
-
+
+ @if (showEndpoint) {
+
+
Endpoint
{{ slug.name }}
Project unique identification In URL
-
-
-
-
{{ environments_with_slug[i]?.name }}
+ } @for (control of forwardUrls.controls; track i; let i = $index) {
+
+
+
{{ environments_with_slug[i]?.name }}
keyboard_arrow_right
-
forward to
+
forward to
{{ environments_with_slug[i]?.name }}
"
>
+ }
-
- {{ fieldConfig?.label }} *
+
+
+ {{ fieldConfig?.label }}
{{ environments_with_slug[i]?.name }}
[formControl]="fieldControl"
/>
-
+ @if (fieldControl?.touched) {
+
{{ environments_with_slug[i]?.name }}
"
>
-
- {{ fieldConfig?.hint }}
+ } @if (fieldConfig?.hint) { {{ fieldConfig?.hint }} }
- {{ customError }}
- {{ label }} is required.
- Min required length is 3
- Start and End spaces are not allowed
- Min value required is {{ minError?.min }}
- Max value allowed is {{ maxError?.max }}
-
- Min required length is {{ minLengthError?.requiredLength }}
-
-
- Max allowed length is {{ maxLengthError?.requiredLength }}
-
-
- {{ patternErrorText ?? 'Enter valid ' + label }}
-
+ @if (fieldControl?.errors?.customError; as customError) {
+ {{ customError }}
+ } @if (fieldControl.errors?.required) {
+ {{ label }} is required. } @if (fieldControl.errors?.minlengthWithSpace) { Min required length is 3 } @if
+ (fieldControl.errors?.noStartEndSpaces) { Start and End spaces are not allowed } @if (fieldControl.errors?.min; as
+ minError) { Min value required is {{ minError?.min }} } @if (fieldControl.errors?.max; as maxError) { Max value
+ allowed is {{ maxError?.max }} } @if (fieldControl.errors?.minlength; as minLengthError) { Min required length is
+ {{ minLengthError?.requiredLength }} } @if (fieldControl.errors?.maxlength; as maxLengthError) { Max allowed length
+ is {{ maxLengthError?.requiredLength }}
+ } @if (fieldControl.errors?.pattern) {
+ {{ patternErrorText ?? 'Enter valid ' + label }}
+ }
diff --git a/apps/proxy-auth-element/src/app/app.component.scss b/apps/36-blocks/src/app/create-project/create-project.component.scss
similarity index 100%
rename from apps/proxy-auth-element/src/app/app.component.scss
rename to apps/36-blocks/src/app/create-project/create-project.component.scss
diff --git a/apps/proxy/src/app/create-project/create-project.component.ts b/apps/36-blocks/src/app/create-project/create-project.component.ts
similarity index 83%
rename from apps/proxy/src/app/create-project/create-project.component.ts
rename to apps/36-blocks/src/app/create-project/create-project.component.ts
index a480bd68..141abef2 100644
--- a/apps/proxy/src/app/create-project/create-project.component.ts
+++ b/apps/36-blocks/src/app/create-project/create-project.component.ts
@@ -1,4 +1,20 @@
-import { Component, OnInit } from '@angular/core';
+import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit, inject } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { RouterModule } from '@angular/router';
+import { ReactiveFormsModule } from '@angular/forms';
+import { MatStepperModule } from '@angular/material/stepper';
+import { MatIconModule } from '@angular/material/icon';
+import { MatCardModule } from '@angular/material/card';
+import { MatButtonModule } from '@angular/material/button';
+import { MatListModule } from '@angular/material/list';
+import { MatSelectModule } from '@angular/material/select';
+import { MatInputModule } from '@angular/material/input';
+import { MatCheckboxModule } from '@angular/material/checkbox';
+import { MatAutocompleteModule } from '@angular/material/autocomplete';
+import { MatTooltipModule } from '@angular/material/tooltip';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { CopyButtonComponent } from '@proxy/ui/copy-button';
+import { MarkAllAsTouchedDirective } from '@proxy/directives/mark-all-as-touched';
import { FormGroup, Validators, FormBuilder, FormControl } from '@angular/forms';
import { CAMPAIGN_NAME_REGEX, ONLY_INTEGER_REGEX, URL_REGEX } from '@proxy/regex';
import { CustomValidators } from '@proxy/custom-validator';
@@ -20,12 +36,36 @@ import {
import { IClientData } from '@proxy/models/users-model';
@Component({
+ changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'proxy-create-project',
+ imports: [
+ CommonModule,
+ RouterModule,
+ ReactiveFormsModule,
+ MatStepperModule,
+ MatIconModule,
+ MatCardModule,
+ MatButtonModule,
+ MatListModule,
+ MatSelectModule,
+ MatInputModule,
+ MatCheckboxModule,
+ MatAutocompleteModule,
+ MatTooltipModule,
+ MatFormFieldModule,
+ CopyButtonComponent,
+ MarkAllAsTouchedDirective,
+ ],
templateUrl: './create-project.component.html',
styleUrls: ['./create-project.component.scss'],
providers: [CreateProjectComponentStore],
})
export class CreateProjectComponent extends BaseComponent implements OnInit {
+ private componentStore = inject(CreateProjectComponentStore);
+ private fb = inject(FormBuilder);
+ private store = inject
>(Store);
+ private cdr = inject(ChangeDetectorRef);
+
public primaryDetailsForm: FormGroup;
public gatewayUrlDetailsForm: FormGroup;
public destinationUrlForm: FormGroup;
@@ -46,11 +86,7 @@ export class CreateProjectComponent extends BaseComponent implements OnInit {
public projectId: number;
public urlUniqId: string;
- constructor(
- private componentStore: CreateProjectComponentStore,
- private fb: FormBuilder,
- private store: Store
- ) {
+ constructor() {
super();
this.projects$ = this.store.pipe(select(selectAllProjectList));
@@ -98,6 +134,7 @@ export class CreateProjectComponent extends BaseComponent implements OnInit {
});
this.populateGatewayUrls();
this.populateForwardUrls();
+ this.cdr.markForCheck();
}
});
});
@@ -118,6 +155,7 @@ export class CreateProjectComponent extends BaseComponent implements OnInit {
});
this.populateGatewayUrls();
this.populateForwardUrls();
+ this.cdr.markForCheck();
}
});
this.createProjectSuccess$.pipe(takeUntil(this.destroy$)).subscribe((res) => {
@@ -125,6 +163,7 @@ export class CreateProjectComponent extends BaseComponent implements OnInit {
this.changeStep(2);
this.store.dispatch(rootActions.getAllProject());
this.getClientData(res.client_id);
+ this.cdr.markForCheck();
}
});
}
diff --git a/apps/proxy/src/app/create-project/create-project.store.ts b/apps/36-blocks/src/app/create-project/create-project.store.ts
similarity index 98%
rename from apps/proxy/src/app/create-project/create-project.store.ts
rename to apps/36-blocks/src/app/create-project/create-project.store.ts
index 85122fb1..afff2ae5 100644
--- a/apps/proxy/src/app/create-project/create-project.store.ts
+++ b/apps/36-blocks/src/app/create-project/create-project.store.ts
@@ -1,6 +1,7 @@
import { Router } from '@angular/router';
import { Injectable } from '@angular/core';
-import { ComponentStore, tapResponse } from '@ngrx/component-store';
+import { ComponentStore } from '@ngrx/component-store';
+import { tapResponse } from '@ngrx/operators';
import { IEnvironments, IProjects } from '@proxy/models/logs-models';
import { BaseResponse, IPaginatedResponse, IReqParams, errorResolver } from '@proxy/models/root-models';
import { CreateProjectService } from '@proxy/services/proxy/create-project';
diff --git a/apps/36-blocks/src/app/dashboard/dashboard.component.html b/apps/36-blocks/src/app/dashboard/dashboard.component.html
new file mode 100644
index 00000000..c3e1994e
--- /dev/null
+++ b/apps/36-blocks/src/app/dashboard/dashboard.component.html
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Coming Soon
+
+
+ Dashboard is under construction
+
+
+
diff --git a/apps/proxy-auth/src/assets/.gitkeep b/apps/36-blocks/src/app/dashboard/dashboard.component.scss
similarity index 100%
rename from apps/proxy-auth/src/assets/.gitkeep
rename to apps/36-blocks/src/app/dashboard/dashboard.component.scss
diff --git a/apps/36-blocks/src/app/dashboard/dashboard.component.ts b/apps/36-blocks/src/app/dashboard/dashboard.component.ts
new file mode 100644
index 00000000..3461c85c
--- /dev/null
+++ b/apps/36-blocks/src/app/dashboard/dashboard.component.ts
@@ -0,0 +1,12 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { RouterModule } from '@angular/router';
+import { MatIconModule } from '@angular/material/icon';
+import { BaseComponent } from '@proxy/ui/base-component';
+
+@Component({
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ selector: 'proxy-dashboard',
+ imports: [RouterModule, MatIconModule],
+ templateUrl: './dashboard.component.html',
+})
+export class DashboardComponent extends BaseComponent {}
diff --git a/apps/36-blocks/src/app/features/create-feature/create-feature.component.html b/apps/36-blocks/src/app/features/create-feature/create-feature.component.html
new file mode 100644
index 00000000..f2263f20
--- /dev/null
+++ b/apps/36-blocks/src/app/features/create-feature/create-feature.component.html
@@ -0,0 +1,2175 @@
+
+ @if ((isLoading$ | async) || (loadingScript | async)) {
}
+
+
+ keyboard_arrow_left
+
+ @if (isEditMode) {
+
+ @if (nameFieldEditMode) {
+
+
+
+ done
+
+
+ } @if (!nameFieldEditMode) {
+
{{ (featureDetails$ | async)?.name }}
+
+ edit
+
+ }
+
+ } @if (!isEditMode) {
+
+
Add New Block
+
Set up a new authentication or subscription block
+
+ }
+
+ @if (!isEditMode) {
+
+
+ @if (featureForm.get('primaryDetails.name'); as nameControl) {
+
+ Block Name
+
+
+
+
+ Next
+ navigate_next
+
+
+
+
+ } @if (featureForm.get('primaryDetails.feature_id')?.value === 1) {
+
+ Configure Method
+
+
+
+
+ navigate_before Back
+
+
+ Nextnavigate_next
+
+
+
+
+ } @if (featureForm.get('primaryDetails.feature_id')?.value === 1) {
+
+ Authorization Setup
+
+
+
+
+ navigate_before Back
+
+
+ Save & Nextnavigate_next
+
+
+
+
+ } @if (featureForm.get('primaryDetails.feature_id')?.value === 1) {
+
+ Branding
+
+
+
+ Update
+
+ Nextnavigate_next
+
+
+
+
+ }
+
+
+ @if (featureForm.get('primaryDetails.feature_id')?.value !== 1) {
+
+ Integration Choice
+
+
+
+
+ navigate_before Back
+
+
+ Nextnavigate_next
+
+
+
+
+
+
+ Organization Details
+
+
+
+
+ navigate_before Back
+
+
+ Nextnavigate_next
+
+
+
+
+
+
+ Billable Metrics / Items
+
+
+
+
+ Nextnavigate_next
+
+
+
+
+
+ Payment Details
+
+
+
+
+ skipnavigate_next
+
+
+
+
+
+
+ Create Plan
+
+
+
+
+ Create Plan & Generate Snippet
+
+
+
+
+
+
+ Plans Overview & Subscription Snippet
+
+
+
+
+
+
+
+ @if (createUpdateObject$ | async; as createUpdateObject) {
+
+ Manage Featurenavigate_next
+
+ }
+
+
+
+ } @if (featureForm.get('primaryDetails.feature_id')?.value === 1) {
+
+ Design & code
+
+
+
+
+ @if (createUpdateObject$ | async; as createUpdateObject) {
+
+ Manage Featurenavigate_next
+
+ }
+
+
+
+ }
+
+ } @if (isEditMode) {
+
+
+
+
+ @if (featureForm.get('primaryDetails.feature_id')?.value === 1) {
+
+ } @if (featureForm.get('primaryDetails.feature_id')?.value !== 1) {
+
+ } @if (featureForm.get('primaryDetails.feature_id')?.value === 1) {
+
+ Update
+
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ bolt
+ Event Catalog
+
+ @if (webhookEventsData?.length) {
+
+ {{ webhookEventsData.length }} events
+
+ }
+
+ @if (webhookEventsData?.length) {
+
+
+ @for (event of webhookEventsData; track event) {
+
+
+
+
+ {{
+ event.value
+ }}
+ {{
+ event.description
+ }}
+
+
+
+
+
+
+ Sample Response
+
+
+ send
+ Try it out
+
+
+
{{ event.sampleResponse | json }}
+
+
+ }
+
+
+ } @else {
+
+
bolt
+
+
No events available
+
+ Events will appear here once configured
+
+
+
+ }
+
+
+
+
+
+
+
+ }
+
+
+
+
+ @for (type of featureType; track type.id) {
+
+
{{ type.icon }}
+
{{ type.name }}
+
+
+ }
+
+
+
+
+
+
+
widgets
+
+
Name your Block
+
Give your authentication block a unique, recognisable name
+
+
+
+
+ @if (!isEditMode && featureForm.get('primaryDetails.feature_id')?.value === 1) {
+
+ }
+
+
+
+
+
+
+
+
+
+ @if (isEditMode) {
+
+
+
+ }
+
+
+
+
+
+ @if ((selectedMethod | async)?.method_services; as methodServices) { @if (!serviceForm) {
+
+ tune
+ {{ methodServices?.[selectedServiceIndex]?.name || 'Configure Service' }}
+
+ } @if (serviceForm ?? featureForm.get('serviceDetails')?.at(selectedServiceIndex); as formToUse) {
+
+
+ @if (methodServices?.[selectedServiceIndex]?.requirements; as requirements) { @if
+ ((formToUse.controls.requirements.controls | keyvalue)?.length) {
+
+ Credentials
+
+ } @for (controlKeyValue of (formToUse.controls.requirements.controls | keyvalue); track
+ controlKeyValue.key) {
+
+ } } @if (methodServices?.[selectedServiceIndex]?.configurations?.fields; as configurationsFields) {
+
+ Configurations
+
+ @for (controlKeyValue of (formToUse.controls.configurations.controls | keyvalue); track
+ controlKeyValue.key) {
+
+ } } @if ((featureDetails$ | async)?.callback_url; as callbackUrl) {
+
+ Callback URL
+
+
+
+ } @if (isEditMode) {
+
+ Enable Service
+
+
+ } @if (isEditMode) {
+
+
+ Reset
+
+
+ }
+
+
+ } }
+
+
+
+
+
+
{{ getSelectedServiceName() }}
+
+ close
+
+
+
+
+
+
+ Reset
+ Done
+
+
+
+
+ @if (selectedMethod | async; as method) {
+
+ @if (configureMethodsTableData.length === 0) {
+
+
tune
+
No methods available
+
+ } @else {
+
+
+ Method
+ Enable
+ Edit
+
+ @for (row of configureMethodsTableData; track row.index) {
+
+
+ {{ row.name }}
+ @if (row.index === 0) {
+
+ default
+
+ }
+
+
+
+
+
+
+ edit
+
+
+
+ }
+
+ }
+
+ Note: By default, 36Blocks credentials will be used, and the consent
+ screen or Sender ID will display the 36Blocks name. You can update these with your own credentials and
+ branding anytime after the block is created.
+
+
+ }
+
+
+
+
+
+ @if (featureForm.get('brandingDetails.version')?.value === 'v2') {
+
+ @if ((logoInputMode === 'file' && logoUrl) || (logoInputMode === 'url' &&
+ featureForm.get('brandingDetails.logo_url')?.value?.startsWith('http'))) {
+
+ }
+
+ {{ featureForm.get('brandingDetails.title')?.value }}
+
+
+ }
+
+
+ @if (previewInputPosition === 'top') {
+
+
+ @if (featureForm.get('brandingDetails.icons').value) {
+
+ } @if (!featureForm.get('brandingDetails.icons').value) {
+
+ } }
+
+
+ @if (previewInputPosition === 'bottom') { @if (featureForm.get('brandingDetails.icons').value) {
+
+ } @if (!featureForm.get('brandingDetails.icons').value) {
+
+ }
+
+
+ }
+
+
+ @if (featureForm.get('brandingDetails.create_account_link').value) {
+
+ Are you a new user?
+ {{ featureForm.get('brandingDetails.sign_up_button_text')?.value }}
+
+ }
+
+
+
+
+
+ @if (featureForm.get('serviceDetails')?.at(3)?.controls?.is_enable?.value &&
+ featureForm.get('brandingDetails.version')?.value === 'v2') {
+
+
+ Email or Phone
+
+
+
+
+ Password
+
+
+
+
+ Sign in
+
+
+ }
+
+
+
+
+ @if ( featureForm.get('serviceDetails')?.at(3)?.controls?.is_enable?.value &&
+ featureForm.get('brandingDetails.version')?.value === 'v2' &&
+ (featureForm.get('serviceDetails')?.at(0)?.controls?.is_enable?.value ||
+ featureForm.get('serviceDetails')?.at(1)?.controls?.is_enable?.value ||
+ featureForm.get('serviceDetails')?.at(2)?.controls?.is_enable?.value) ) {
+
+
+ Or continue with
+
+
+ }
+
+
+
+ @if (featureForm.get('serviceDetails')?.at(0)?.controls?.is_enable?.value) {
+
phone_android
+ } @if (featureForm.get('serviceDetails')?.at(1)?.controls?.is_enable?.value) {
+
+ } @if (featureForm.get('serviceDetails')?.at(2)?.controls?.is_enable?.value) {
+
apple
+ } @if (featureForm.get('serviceDetails')?.at(3)?.controls?.is_enable?.value &&
+ featureForm.get('brandingDetails.version')?.value === 'v1') {
+
password
+ }
+
+
+
+
+
+
+ @if (featureForm.get('serviceDetails')?.at(0)?.controls?.is_enable?.value) {
+
+
+ phone_android
+ Login With OTP
+
+
+ } @if (featureForm.get('serviceDetails')?.at(2)?.controls?.is_enable?.value) {
+
+
+ apple
+ Continue with Apple
+
+
+ } @if (featureForm.get('serviceDetails')?.at(1)?.controls?.is_enable?.value) {
+
+
+
+
Continue with Google
+
+
+ } @if (featureForm.get('serviceDetails')?.at(3)?.controls?.is_enable?.value &&
+ featureForm.get('brandingDetails.version')?.value === 'v1') {
+
+
+ password
+ Continue with Password
+
+
+ }
+
+
+
+
+
+
+
+
+
+ settings
+ Session & Token Settings
+
+
+
+
+
+ @if (isEditMode && featureForm.get('primaryDetails.feature_id')?.value === 1) {
+
+
+ Block new user sign-ups
+ Prevent new users from registering via this block
+
+
+
+ }
+
+
+
+ @if (((isEditMode ? featureDetails$ : selectedMethod) | async)?.authorization_format?.format; as code) {
+
+
+
+ data_object
+ JWT Payload Format
+
+
+
+
+
+
+
+
info
+
+ JSON format you will receive after login as JWT in Authorization key
+
+
+
+ }
+
+ @if (isEditMode) {
+
+
+
+ }
+
+
+
+
+
+
+
+
+ code
+ Integration Script
+
+
+
+
+
+
+
+
+
+
+
+ html
+ Button Container Div
+
+
+
+
+
+
+
+
info
+
+ The social login button is shown in a dialog by default. Add this div to your UI to render it inline
+ instead.
+
+
+
+
+
+
+
+ @if ((createUpdateObject$ | async) || (featureDetails$ | async)) {
+
+ Preview
+
+ }
+
+
+
+
+
+
+ webhook
+ Webhook Configuration
+
+
+
+
+
+ Select Trigger Events
+
+ @for (option of webhookEventsData; track option.value) {
+ {{ option.label }}
+ }
+
+
+
+
+
+
+
+
+
+
+ {{ fieldConfig?.label }}
+
+
+ @switch (fieldConfig?.type) { @case (featureFieldType.ChipList) { @if (fieldConfig?.label + '_' +
+ this.selectedServiceIndex; as chipListKey) {
+
+ @for (item of chipListValues[chipListKey]; track item) {
+
+ {{ item }}
+ @if (!chipListReadOnlyValues[chipListKey].has(item)) {
+
+ cancel
+
+ }
+
+ }
+
+
+ } } @case (featureFieldType.Select) {
+
+ @if (fieldConfig?.create_new) {
+
+ add Add New
+ {{ fieldConfig?.label }}
+
+ } @for (option of getSelectOptions(fieldConfig); track option.value) {
+ {{ option.label }}
+ }
+
+ } @case (featureFieldType.TextArea) {
+
+ } @case (featureFieldType.ReadFile) {
+
+ } @case (featureFieldType.Password) {
+
+ @if (fieldConfig?.info) {
+ info
+ } } @default {
+
+ @if (fieldConfig?.info) {
+ info
+ } } } @if (fieldControl.touched) {
+
+
+
+ } @if (fieldConfig?.type === featureFieldType.ReadFile) { @if (fieldConfig?.label + '_' +
+ this.selectedServiceIndex; as fileKey) {
+
+ @if (!fieldControl?.value) {
+
+ attach_file
+
+ } @else {
+
+ close
+
+ } } } @if (fieldConfig?.hint) {
+
+ {{ fieldConfig?.hint }}
+ @if (fieldConfig?.hintInfo) {
+ info
+ }
+
+ }
+
+
+ @if (fieldConfig?.type === featureFieldType.ChipList) {
+
+ @if (fieldControl.touched) {
+
+
+
+ }
+
+ }
+
+
+ @if (fieldControl?.errors?.customError; as customError) {
+ {{ customError }}
+ } @if (fieldControl.errors?.required) {
+ {{ label }} is required. } @if (fieldControl.errors?.minlengthWithSpace) { Min required length is 3 } @if
+ (fieldControl.errors?.noStartEndSpaces) { Start and End spaces are not allowed } @if (fieldControl.errors?.min;
+ as minError) { Min value required is {{ minError?.min }} } @if (fieldControl.errors?.max; as maxError) { Max
+ value allowed is {{ maxError?.max }} } @if (fieldControl.errors?.minlength; as minLengthError) { Min required
+ length is {{ minLengthError?.requiredLength }} } @if (fieldControl.errors?.maxlength; as maxLengthError) { Max
+ allowed length is {{ maxLengthError?.requiredLength }}
+ } @if (fieldControl.errors?.pattern) {
+ {{ patternErrorText ?? 'Enter valid ' + label }}
+ } @if (fieldControl?.errors?.atleastOneValueInChipList) { Atleast One Value is Required }
+
+
+
+
+
+
Integration Choice
+
+
+ @for (service of (selectedMethod | async)?.method_services; track service; let i = $index) {
+
+
{{ service.name }}
+
+
+ @if (featureForm.get('serviceDetails')?.at(i); as serviceForm) { @if (i !== selectedServiceIndex &&
+ serviceForm.dirty && serviceForm.touched && serviceForm.invalid) {
+
error
+ } }
+
+
+
+
+ }
+
+
+
+
+
+
+
+
Organization Details
+
Note: These details will be shown on the invoice..
+
+
+
+
+
+
+
+
Billable Metrics / Items
+
+ add Add Metric
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Payment Details
+
+
Note: These details will be can be filled later.
+
+
+ @if (featureForm.get('paymentDetailsForm')?.at(0); as paymentDetailsForm) {
+
+ @for (controlKeyValue of (paymentDetailsForm.controls.stripe.controls | keyvalue); track
+ controlKeyValue.key) {
+
+ }
+
+
+
+ {{ isEditMode ? 'Update' : 'Save' }}
+
+
+ }
+
+
+
+
+
+
+
+
+
Create Plan
+
+
+
+ @if ((selectedMethod | async)?.method_services; as methodServices) { @if
+ (featureForm.get('planDetails')?.at(selectedServiceIndex); as serviceForm) {
+
+
+
+
+
+
+
+ Charges
+
+
+
+
+ @if (chargesList?.length) {
+
+
+
+
+ Metric
+
+ {{
+ getBillableMetricName(
+ charge.billable_metric_id || charge.lago_billable_metric_code
+ ) || 'N/A'
+ }}
+
+
+
+
+
+ Max Limit
+
+ {{ charge.max_limit || 'N/A' }}
+
+
+
+
+
+ Actions
+
+
+ delete
+
+
+
+
+
+
+
+
+ }
+
+
+
+ } }
+
+
+
+
+
+
+
+
+
+
+
+
Created Plans
+ @if (isEditMode && selectedSubscriptionServiceIndex === -1) {
+
+ add Add Plan
+
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Taxes
+
+ add Add Tax
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Services
+
+
+
+ Billable Metric
+ navigate_next
+
+
+
+
+ Plans
+ navigate_next
+
+
+
+ Payment Details
+ navigate_next
+
+
+ Taxes
+ navigate_next
+
+
+
+
+
+
+
+ @switch (selectedSubscriptionServiceIndex) { @case (-1) {
+
+ } @case (-2) {
+
+ } @case (-3) {
+
+ } @case (-4) {
+
+ } }
+
+
+
+
+
+
+
+
+
+
+
+ @if (isEditMode && featureForm.get('serviceDetails')?.at(3)?.controls?.is_enable?.value &&
+ featureForm.get('brandingDetails.version')?.value === 'v2') {
+
+
+
+ Input on Top
+
+
+ Input on Bottom
+
+
+
+ }
+
+
+
+
diff --git a/apps/36-blocks/src/app/features/create-feature/create-feature.component.scss b/apps/36-blocks/src/app/features/create-feature/create-feature.component.scss
new file mode 100644
index 00000000..e1d66c2b
--- /dev/null
+++ b/apps/36-blocks/src/app/features/create-feature/create-feature.component.scss
@@ -0,0 +1,320 @@
+.code-snippet-view {
+ transition: min-height 0.2s linear;
+ min-width: 500px;
+ max-width: 800px;
+}
+
+.active {
+ background-color: var(--color-dark-accent-light) !important;
+ border-color: var(--color-dark-accent) !important;
+}
+
+// Show Configure column edit button only on row hover
+// .configure-methods-table {
+// tr.mat-row,
+// tr.mat-mdc-row {
+// .mat-column-configure button {
+// opacity: 0;
+// transition: opacity 0.15s ease;
+// }
+// &:hover .mat-column-configure button {
+// opacity: 1;
+// }
+// }
+// th {
+// font-size: var(--font-size-common-14) !important;
+// }
+// }
+
+// Fix table borders
+// .default-table {
+// .mat-mdc-row {
+// border-bottom: 1px solid rgba(0, 0, 0, 0.12);
+
+// &:last-child {
+// border-bottom: 1px solid rgba(0, 0, 0, 0.12);
+// }
+// }
+
+// .mat-mdc-header-row {
+// border-bottom: 2px solid rgba(0, 0, 0, 0.12);
+// }
+
+// .mat-mdc-cell,
+// .mat-mdc-header-cell {
+// border-right: 1px solid rgba(0, 0, 0, 0.12);
+
+// &:last-child {
+// border-right: none;
+// }
+// }
+// }
+.organization-details-form {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 24px;
+ align-items: start;
+
+ @media (max-width: 768px) {
+ grid-template-columns: 1fr;
+ gap: 16px;
+ }
+
+ .heading {
+ font-size: 16px;
+ line-height: 20px;
+ font-weight: 600;
+ }
+ textarea {
+ width: 100%;
+ resize: vertical;
+ min-height: 50px;
+ }
+}
+
+.single-form-layout {
+ .form-section {
+ .form-grid {
+ grid-template-columns: repeat(2, 1fr);
+
+ @media (max-width: 768px) {
+ grid-template-columns: 1fr;
+ gap: 16px;
+ }
+
+ textarea {
+ width: 100%;
+ resize: vertical;
+ min-height: 80px;
+ }
+ }
+ }
+}
+
+.charges-card {
+ margin-top: 24px;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
+ border: 2px solid var(--color-common-border);
+ border-radius: 12px;
+ background-color: var(--color-common-bg);
+ animation: slideInUp 0.5s ease-out;
+
+ mat-card-header {
+ border-radius: 12px 12px 0 0;
+ }
+
+ .form-grid {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .button-group {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-top: 16px;
+ }
+}
+
+@keyframes slideInUp {
+ from {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.charges-table-container {
+ margin-top: 24px;
+ border: 1px solid var(--color-common-border);
+ border-radius: 8px;
+ overflow: hidden;
+ width: 100%;
+
+ .charges-table {
+ width: 100% !important;
+ min-width: 100% !important;
+ table-layout: fixed;
+ margin: 0;
+ }
+
+ .add-plan-edit {
+ height: calc(100vh - 650px);
+ overflow: auto;
+ }
+}
+
+.info-icon {
+ // font-size: 17px;
+ opacity: 0.7;
+ transition: opacity 0.2s ease-in-out;
+ position: absolute;
+ right: 4px;
+ // top: 4px;
+ z-index: 1;
+
+ &:hover {
+ opacity: 1;
+ }
+}
+
+.code-block {
+ background-color: #000000;
+ color: #ffffff;
+ padding: 16px;
+ border-radius: 8px;
+ overflow-x: auto;
+ font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
+ font-size: 12px;
+ line-height: 1.6;
+ max-height: 350px;
+ overflow-y: auto;
+ margin: 0;
+
+ code {
+ white-space: pre-wrap;
+ word-break: break-word;
+ }
+}
+
+.bg-dark {
+ background-color: #616161 !important;
+ color: #ffffff !important;
+}
+.text-link {
+ color: var(--color-common-primary) !important;
+}
+
+.preview-input-field {
+ input {
+ border: none;
+ outline: none;
+ background: transparent;
+ width: 100%;
+
+ &::placeholder {
+ color: var(--color-common-text);
+ opacity: 1;
+ }
+ }
+}
+
+.custom-toggle-group {
+ display: inline-flex;
+ border-radius: 8px;
+ overflow: hidden;
+ border: 1px solid var(--color-common-border);
+ background-color: var(--color-common-bg);
+
+ .custom-toggle-btn {
+ padding: 8px 20px;
+ font-size: 13px;
+ font-weight: 500;
+ border: none;
+ background-color: transparent;
+ color: var(--color-common-secondary-text);
+ cursor: pointer;
+ transition: all 0.2s ease;
+ outline: none;
+
+ &:not(:last-child) {
+ border-right: 1px solid var(--color-common-border);
+ }
+
+ &:hover:not(.active) {
+ background-color: var(--color-common-hover);
+ }
+
+ &.active {
+ background-color: var(--color-common-white);
+ color: var(--color-common-primary);
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ }
+ }
+}
+
+.social-login-icon-box {
+ border: 1px solid var(--color-common-border);
+ border-radius: 8px;
+ padding: 12px 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ img {
+ width: 20px;
+ height: 20px;
+ }
+}
+
+.auth-option-btn {
+ width: 85%;
+ height: 44px;
+ border: 1px solid var(--color-common-border) !important;
+ border-radius: 8px !important;
+ font-size: 14px;
+ font-weight: 600;
+ background-color: var(--color-common-white) !important;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ &.dark-theme {
+ background-color: transparent !important;
+ border-color: #ffffff !important;
+ }
+}
+
+.auth-option-content {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ width: 180px;
+}
+
+.auth-option-icon {
+ height: 24px;
+ width: 24px;
+ min-width: 24px;
+ margin-right: 12px;
+
+ &.dark-theme {
+ filter: invert(1);
+ }
+}
+
+.auth-option-text {
+ white-space: nowrap;
+
+ &.dark-theme {
+ color: #ffffff;
+ }
+}
+
+.auth-credentials {
+ border-radius: var(--branding-border-radius, 8px) !important;
+ // mat-form-field and other elements use --border-common-radius-4; override with branding radius
+ --border-common-radius-4: var(--branding-border-radius, 8px);
+}
+.auth-credentials .branding-preview-btn {
+ background-color: var(--branding-button-color, #19e6ce) !important;
+ color: var(--branding-button-text-color, #000000) !important;
+ border-radius: var(--branding-border-radius, 8px) !important;
+}
+.auth-credentials .branding-preview-btn:hover {
+ background-color: var(--branding-button-hover-color, #19e6ce) !important;
+}
+.auth-credentials .auth-option-btn {
+ border-radius: var(--branding-border-radius, 8px) !important;
+}
+.auth-credentials .social-login-icon-box {
+ border-radius: var(--branding-border-radius, 8px) !important;
+}
+
+.branding-preview-logo {
+ max-height: 48px;
+ max-width: 160px;
+ object-fit: contain;
+}
diff --git a/apps/proxy/src/app/features/create-feature/create-feature.component.ts b/apps/36-blocks/src/app/features/create-feature/create-feature.component.ts
similarity index 95%
rename from apps/proxy/src/app/features/create-feature/create-feature.component.ts
rename to apps/36-blocks/src/app/features/create-feature/create-feature.component.ts
index 6c64be98..4d3b097d 100644
--- a/apps/proxy/src/app/features/create-feature/create-feature.component.ts
+++ b/apps/36-blocks/src/app/features/create-feature/create-feature.component.ts
@@ -1,6 +1,28 @@
-import { ActivatedRoute } from '@angular/router';
+import { ActivatedRoute, RouterModule } from '@angular/router';
+import { CommonModule } from '@angular/common';
+import { ReactiveFormsModule, FormsModule } from '@angular/forms';
+import { MatInputModule } from '@angular/material/input';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatIconModule } from '@angular/material/icon';
+import { MatCardModule } from '@angular/material/card';
+import { MatButtonModule } from '@angular/material/button';
+import { LoaderComponent } from '@proxy/ui/loader';
+import { MatStepperModule } from '@angular/material/stepper';
+import { MatListModule } from '@angular/material/list';
+import { MatChipsModule } from '@angular/material/chips';
+import { MatTabsModule } from '@angular/material/tabs';
+import { MatSlideToggleModule } from '@angular/material/slide-toggle';
+import { CopyButtonComponent } from '@proxy/ui/copy-button';
+import { MatTableModule } from '@angular/material/table';
+import { MatDialogModule } from '@angular/material/dialog';
+import { MatSelectModule } from '@angular/material/select';
+import { MatTooltipModule } from '@angular/material/tooltip';
+import { MatExpansionModule } from '@angular/material/expansion';
+import { MatRadioModule } from '@angular/material/radio';
+import { MarkdownModule } from 'ngx-markdown';
import { cloneDeep, isEqual } from 'lodash-es';
import {
+ ChangeDetectionStrategy,
Component,
OnDestroy,
OnInit,
@@ -12,6 +34,8 @@ import {
ElementRef,
AfterViewInit,
TemplateRef,
+ inject,
+ signal,
} from '@angular/core';
import { BaseComponent } from '@proxy/ui/base-component';
import { BehaviorSubject, Observable, distinctUntilChanged, filter, of, take, takeUntil } from 'rxjs';
@@ -28,6 +52,7 @@ import {
ProxyAuthScript,
ProxyAuthScriptUrl,
} from '@proxy/models/features-model';
+import { PublicScriptType, WidgetTheme } from '@proxy/constant';
import { AbstractControl, FormArray, FormControl, FormGroup, Validators, ValidatorFn } from '@angular/forms';
import { CAMPAIGN_NAME_REGEX, ONLY_INTEGER_REGEX, URL_REGEX } from '@proxy/regex';
import { CustomValidators } from '@proxy/custom-validator';
@@ -36,13 +61,13 @@ import { environment } from '../../../environments/environment';
import { HttpClient } from '@angular/common/http';
import { PrimeNgToastService } from '@proxy/ui/prime-ng-toast';
import { MatStepper } from '@angular/material/stepper';
-import { MatDialog } from '@angular/material/dialog';
-import { MatDialogRef } from '@angular/material/dialog';
+import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { getAcceptedTypeRegex } from '@proxy/utils';
import { SimpleDialogComponent } from './simple-dialog/simple-dialog.component';
import { CreatePlanDialogComponent } from './create-plan-dialog/create-plan-dialog.component';
import { CreateTaxDialogComponent } from './create-tax-dialog/create-tax-dialog.component';
import { ConfirmDialogComponent } from '@proxy/ui/confirm-dialog';
+import { ServiceListComponent } from '@proxy/ui/service-list';
type ServiceFormGroup = FormGroup<{
requirements: FormGroup<{
[key: string]: FormControl;
@@ -82,7 +107,34 @@ export interface PeriodicElement {
code?: string;
}
@Component({
+ changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'proxy-create-feature',
+ imports: [
+ CommonModule,
+ RouterModule,
+ ReactiveFormsModule,
+ FormsModule,
+ MatInputModule,
+ MatFormFieldModule,
+ MatIconModule,
+ MatCardModule,
+ MatButtonModule,
+ LoaderComponent,
+ MatStepperModule,
+ MatListModule,
+ MatChipsModule,
+ MarkdownModule,
+ MatTabsModule,
+ MatSlideToggleModule,
+ CopyButtonComponent,
+ MatTableModule,
+ MatDialogModule,
+ MatSelectModule,
+ MatTooltipModule,
+ MatExpansionModule,
+ MatRadioModule,
+ ServiceListComponent,
+ ],
templateUrl: './create-feature.component.html',
styleUrls: ['./create-feature.component.scss'],
providers: [CreateFeatureComponentStore],
@@ -93,6 +145,15 @@ export class CreateFeatureComponent extends BaseComponent implements OnDestroy,
@ViewChild('authorizationStepContent', { read: ElementRef }) authorizationStepContent: ElementRef;
@ViewChild('configureMethodDialogTemplate', { read: TemplateRef })
configureMethodDialogTemplateRef: TemplateRef;
+
+ private componentStore = inject(CreateFeatureComponentStore);
+ private cdr = inject(ChangeDetectorRef);
+ private activatedRoute = inject(ActivatedRoute);
+ private toast = inject(PrimeNgToastService);
+ private ngZone = inject(NgZone);
+ private dialog = inject(MatDialog);
+ private http = inject(HttpClient);
+
public taxes: any[] = [];
public createPlanForm: any;
public taxConfigData: any;
@@ -169,6 +230,7 @@ export class CreateFeatureComponent extends BaseComponent implements OnDestroy,
public paymentDetailsFormFields: any;
public paymentDetailsData: any;
public featureFieldType = FeatureFieldType;
+ protected readonly WidgetTheme = WidgetTheme;
public proxyAuthScript = ProxyAuthScript(environment.proxyServer);
public configureMethodsTableColumns: string[] = ['method', 'toggle', 'configure'];
@@ -250,19 +312,8 @@ export class CreateFeatureComponent extends BaseComponent implements OnDestroy,
}),
// New form controls for conditional steps
});
- public demoDiv$: Observable = of(null);
+ public demoDiv = signal(null);
public keepOrder = () => 0;
- constructor(
- private componentStore: CreateFeatureComponentStore,
- private cdr: ChangeDetectorRef,
- private activatedRoute: ActivatedRoute,
- private toast: PrimeNgToastService,
- private ngZone: NgZone,
- private dialog: MatDialog,
- private http: HttpClient
- ) {
- super();
- }
ngOnInit(): void {
this.componentStore.getWebhookEvents();
@@ -307,7 +358,7 @@ export class CreateFeatureComponent extends BaseComponent implements OnDestroy,
feature.reference_id,
feature.feature_id === 1 ? 'authorization' : 'subscription'
);
- this.demoDiv$ = of(`
`);
+ this.demoDiv.set(`
`);
// Initialize billable metrics form fields for edit mode
if (feature.feature_id === 2) {
@@ -449,7 +500,7 @@ export class CreateFeatureComponent extends BaseComponent implements OnDestroy,
}, 10);
}
this.featureId = obj.id;
- this.demoDiv$ = of(`
`);
+ this.demoDiv.set(`
`);
});
this.createBillableMetric$.pipe(filter(Boolean), takeUntil(this.destroy$)).subscribe((metric) => {
@@ -642,8 +693,8 @@ export class CreateFeatureComponent extends BaseComponent implements OnDestroy,
public getEffectivePrimaryColor(): string {
const theme = this.featureForm.get('brandingDetails.theme')?.value;
const isDark =
- theme === 'dark' ||
- (theme === 'system' &&
+ theme === WidgetTheme.Dark ||
+ (theme === WidgetTheme.System &&
typeof localStorage !== 'undefined' &&
localStorage.getItem('selected-theme') === 'dark-theme');
if (isDark) {
@@ -984,6 +1035,9 @@ export class CreateFeatureComponent extends BaseComponent implements OnDestroy,
}
}
+ public getServiceFormAt = (index: number): AbstractControl | null =>
+ (this.featureForm.get('serviceDetails') as FormArray)?.at(index) ?? null;
+
public get isConfigureMethodValid(): boolean {
let isValid = true;
const serviceFormArray = this.featureForm.controls.serviceDetails;
@@ -1215,9 +1269,8 @@ export class CreateFeatureComponent extends BaseComponent implements OnDestroy,
public deleteMetric(index: number): void {
const dialogRef: MatDialogRef = this.dialog.open(ConfirmDialogComponent);
- const componentInstance = dialogRef.componentInstance;
- componentInstance.confirmationMessage = `Are you sure to delete this metric?`;
- componentInstance.confirmButtonText = 'Delete';
+ dialogRef.componentRef.setInput('confirmationMessage', `Are you sure to delete this metric?`);
+ dialogRef.componentRef.setInput('confirmButtonText', 'Delete');
dialogRef.afterClosed().subscribe((action) => {
if (action === 'yes') {
this.componentStore.deleteBillableMetric({
@@ -1319,7 +1372,7 @@ export class CreateFeatureComponent extends BaseComponent implements OnDestroy,
referenceId:
this.getValueFromObservable(this.createUpdateObject$)?.reference_id ??
this.getValueFromObservable(this.featureDetails$)?.reference_id,
- type: featureId === 1 ? 'authorization' : 'subscription',
+ type: featureId === 1 ? PublicScriptType.Authorization : PublicScriptType.Subscription,
isPreview: true,
target: '_blank',
success: (data) => {
@@ -1784,9 +1837,8 @@ export class CreateFeatureComponent extends BaseComponent implements OnDestroy,
}
public deletePlan(plan: any): void {
const dialogRef: MatDialogRef = this.dialog.open(ConfirmDialogComponent);
- const componentInstance = dialogRef.componentInstance;
- componentInstance.confirmationMessage = `Are you sure to delete this metric?`;
- componentInstance.confirmButtonText = 'Delete';
+ dialogRef.componentRef.setInput('confirmationMessage', `Are you sure to delete this metric?`);
+ dialogRef.componentRef.setInput('confirmButtonText', 'Delete');
dialogRef.afterClosed().subscribe((action) => {
if (action === 'yes') {
this.componentStore.deletePlan({ refId: this.getReferenceId(), code: plan.code });
@@ -1934,9 +1986,8 @@ export class CreateFeatureComponent extends BaseComponent implements OnDestroy,
}
public deleteTax(tax: any): void {
const dialogRef: MatDialogRef = this.dialog.open(ConfirmDialogComponent);
- const componentInstance = dialogRef.componentInstance;
- componentInstance.confirmationMessage = `Are you sure to delete this tax?`;
- componentInstance.confirmButtonText = 'Delete';
+ dialogRef.componentRef.setInput('confirmationMessage', `Are you sure to delete this tax?`);
+ dialogRef.componentRef.setInput('confirmButtonText', 'Delete');
dialogRef.afterClosed().subscribe((action) => {
if (action === 'yes') {
this.componentStore.deleteTax({ refId: this.getReferenceId(), code: tax.code });
@@ -2059,4 +2110,8 @@ export class CreateFeatureComponent extends BaseComponent implements OnDestroy,
this.configureMethodDialogForm.markAllAsTouched();
}
}
+
+ handleFileUpload(fileInput: HTMLInputElement): void {
+ fileInput?.click();
+ }
}
diff --git a/apps/proxy/src/app/features/create-feature/create-feature.store.ts b/apps/36-blocks/src/app/features/create-feature/create-feature.store.ts
similarity index 99%
rename from apps/proxy/src/app/features/create-feature/create-feature.store.ts
rename to apps/36-blocks/src/app/features/create-feature/create-feature.store.ts
index 2b1e8b7c..70c41567 100644
--- a/apps/proxy/src/app/features/create-feature/create-feature.store.ts
+++ b/apps/36-blocks/src/app/features/create-feature/create-feature.store.ts
@@ -1,7 +1,8 @@
import { Injectable } from '@angular/core';
import { BaseResponse, errorResolver } from '@proxy/models/root-models';
import { PrimeNgToastService } from '@proxy/ui/prime-ng-toast';
-import { ComponentStore, tapResponse } from '@ngrx/component-store';
+import { ComponentStore } from '@ngrx/component-store';
+import { tapResponse } from '@ngrx/operators';
import { EMPTY, Observable, catchError, switchMap } from 'rxjs';
import { IFeature, IFeatureDetails, IFeatureType, IMethod } from '@proxy/models/features-model';
import { FeaturesService } from '@proxy/services/proxy/features';
@@ -231,7 +232,7 @@ export class CreateFeatureComponentStore extends ComponentStore) => {
return data.pipe(
switchMap((id) => {
- this.patchState({ isLoading: true });
+ this.patchState({ isLoading: true, featureDetails: null });
return this.service.getFeatureDetails(id).pipe(
tapResponse(
(res: BaseResponse) => {
diff --git a/apps/proxy/src/app/features/create-feature/create-plan-dialog/create-plan-dialog.component.ts b/apps/36-blocks/src/app/features/create-feature/create-plan-dialog/create-plan-dialog.component.ts
similarity index 84%
rename from apps/proxy/src/app/features/create-feature/create-plan-dialog/create-plan-dialog.component.ts
rename to apps/36-blocks/src/app/features/create-feature/create-plan-dialog/create-plan-dialog.component.ts
index acb03158..8137c145 100644
--- a/apps/proxy/src/app/features/create-feature/create-plan-dialog/create-plan-dialog.component.ts
+++ b/apps/36-blocks/src/app/features/create-feature/create-plan-dialog/create-plan-dialog.component.ts
@@ -1,4 +1,16 @@
-import { Component, Inject, OnInit, OnDestroy } from '@angular/core';
+import { ChangeDetectionStrategy, Component, OnInit, OnDestroy, inject } from '@angular/core';
+import { NgTemplateOutlet } from '@angular/common';
+import { ReactiveFormsModule } from '@angular/forms';
+import { MatButtonModule } from '@angular/material/button';
+import { MatCardModule } from '@angular/material/card';
+import { MatIconModule } from '@angular/material/icon';
+import { MatDialogModule } from '@angular/material/dialog';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatInputModule } from '@angular/material/input';
+import { MatSelectModule } from '@angular/material/select';
+import { MatChipsModule } from '@angular/material/chips';
+import { MatSlideToggleModule } from '@angular/material/slide-toggle';
+import { MatRadioModule } from '@angular/material/radio';
import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA, MatDialog } from '@angular/material/dialog';
import { CustomValidators } from '@proxy/custom-validator';
@@ -6,7 +18,22 @@ import { BaseComponent } from '@proxy/ui/base-component';
import { COMMA, ENTER } from '@angular/cdk/keycodes';
@Component({
+ changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'proxy-create-plan-dialog',
+ imports: [
+ NgTemplateOutlet,
+ ReactiveFormsModule,
+ MatButtonModule,
+ MatCardModule,
+ MatIconModule,
+ MatDialogModule,
+ MatFormFieldModule,
+ MatInputModule,
+ MatSelectModule,
+ MatChipsModule,
+ MatSlideToggleModule,
+ MatRadioModule,
+ ],
template: `
{{ dialogTitle }}
@@ -25,17 +52,17 @@ import { COMMA, ENTER } from '@angular/cdk/keycodes';
@@ -46,17 +73,17 @@ import { COMMA, ENTER } from '@angular/cdk/keycodes';