Sign in to your account
>
Or continue with
@@ -774,7 +932,7 @@
Sign in to your account
@@ -790,19 +948,19 @@
Sign in to your account
*ngIf="featureForm.get('serviceDetails')?.at(0)?.controls?.is_enable?.value"
type="button"
class="auth-option-btn"
- [class.dark-theme]="featureForm.get('authorizationDetails.theme')?.value === 'dark'"
+ [class.dark-theme]="featureForm.get('brandingDetails.theme')?.value === 'dark'"
>
phone_android
Login With OTP
@@ -811,19 +969,19 @@
Sign in to your account
*ngIf="featureForm.get('serviceDetails')?.at(2)?.controls?.is_enable?.value"
type="button"
class="auth-option-btn"
- [class.dark-theme]="featureForm.get('authorizationDetails.theme')?.value === 'dark'"
+ [class.dark-theme]="featureForm.get('brandingDetails.theme')?.value === 'dark'"
>
apple
Continue with Apple
@@ -832,13 +990,13 @@
Sign in to your account
*ngIf="featureForm.get('serviceDetails')?.at(1)?.controls?.is_enable?.value"
type="button"
class="auth-option-btn"
- [class.dark-theme]="featureForm.get('authorizationDetails.theme')?.value === 'dark'"
+ [class.dark-theme]="featureForm.get('brandingDetails.theme')?.value === 'dark'"
>
Continue with Google
@@ -846,23 +1004,23 @@
Sign in to your account
+
+
+
+
+
+
+
+
+
+
+ Input on Top
+
+
+ Input on Bottom
+
+
+
+
+
+
+
diff --git a/apps/proxy/src/app/features/create-feature/create-feature.component.scss b/apps/proxy/src/app/features/create-feature/create-feature.component.scss
index 80f39a9c..1afd4dda 100644
--- a/apps/proxy/src/app/features/create-feature/create-feature.component.scss
+++ b/apps/proxy/src/app/features/create-feature/create-feature.component.scss
@@ -21,6 +21,28 @@
width: 250px;
}
+.configure-method-dialog-content {
+ max-height: 65vh;
+ overflow-y: auto;
+}
+
+// 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 {
@@ -409,7 +431,6 @@
.info-icon {
color: var(--color-common-primary, #1976d2);
font-size: 17px;
- cursor: help;
opacity: 0.7;
transition: opacity 0.2s ease-in-out;
position: absolute;
@@ -575,7 +596,6 @@
color: #ffffff !important;
}
.mat-form-field-outline-thick {
- background-color: #fffcfc !important;
color: #ffffff !important;
}
.mat-form-field-outline-start,
@@ -893,3 +913,69 @@
}
}
}
+
+// Branding step: apply border radius to all preview elements (variable --branding-border-radius set on .auth-credentials)
+.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;
+}
+
+// Email or Phone & Password fields in preview: apply user's border radius to outline
+::ng-deep .auth-credentials .branding-preview-input .mdc-text-field--outlined {
+ --mdc-outlined-text-field-container-shape: var(--branding-border-radius, 8px) !important;
+}
+::ng-deep .auth-credentials .branding-preview-input .mdc-notched-outline .mdc-notched-outline__leading {
+ border-top-left-radius: var(--branding-border-radius, 8px) !important;
+ border-bottom-left-radius: var(--branding-border-radius, 8px) !important;
+}
+::ng-deep .auth-credentials .branding-preview-input .mdc-notched-outline .mdc-notched-outline__trailing {
+ border-top-right-radius: var(--branding-border-radius, 8px) !important;
+ border-bottom-right-radius: var(--branding-border-radius, 8px) !important;
+}
+::ng-deep .auth-credentials .branding-preview-input .mdc-notched-outline .mdc-notched-outline__notch {
+ border-top-left-radius: var(--branding-border-radius, 8px) !important;
+ border-top-right-radius: var(--branding-border-radius, 8px) !important;
+}
+
+.branding-preview-logo {
+ max-height: 48px;
+ max-width: 160px;
+ object-fit: contain;
+}
+
+.branding-step-content {
+ min-height: 400px;
+}
+
+// Use app green theme for logo radio buttons (instead of default blue)
+::ng-deep .branding-logo-radio {
+ .mat-mdc-radio-button.mat-mdc-radio-checked .mdc-radio__outer-circle,
+ .mat-mdc-radio-button .mdc-radio__outer-circle {
+ border-color: var(--color-dark-accent) !important;
+ }
+ .mat-mdc-radio-button.mat-mdc-radio-checked .mdc-radio__inner-circle {
+ border-color: var(--color-dark-accent) !important;
+ background-color: var(--color-dark-accent) !important;
+ }
+ .mat-mdc-radio-button .mdc-radio__background::before {
+ background-color: var(--color-dark-accent) !important;
+ }
+}
+.mat-tab-body-wrapper {
+ height: 100%;
+}
diff --git a/apps/proxy/src/app/features/create-feature/create-feature.component.ts b/apps/proxy/src/app/features/create-feature/create-feature.component.ts
index 95bb7b8f..21480d06 100644
--- a/apps/proxy/src/app/features/create-feature/create-feature.component.ts
+++ b/apps/proxy/src/app/features/create-feature/create-feature.component.ts
@@ -1,11 +1,24 @@
import { ActivatedRoute } from '@angular/router';
import { cloneDeep, isEqual } from 'lodash-es';
-import { Component, OnDestroy, OnInit, NgZone, ViewChildren, QueryList, ChangeDetectorRef } from '@angular/core';
+import {
+ Component,
+ OnDestroy,
+ OnInit,
+ NgZone,
+ ViewChildren,
+ QueryList,
+ ChangeDetectorRef,
+ ViewChild,
+ ElementRef,
+ AfterViewInit,
+ TemplateRef,
+} from '@angular/core';
import { BaseComponent } from '@proxy/ui/base-component';
import { BehaviorSubject, Observable, distinctUntilChanged, filter, of, take, takeUntil } from 'rxjs';
import { CreateFeatureComponentStore } from './create-feature.store';
import {
FeatureFieldType,
+ FeatureServiceIds,
IFeature,
IFeatureDetails,
IFeatureType,
@@ -15,7 +28,7 @@ import {
ProxyAuthScript,
ProxyAuthScriptUrl,
} from '@proxy/models/features-model';
-import { FormArray, FormControl, FormGroup, Validators, ValidatorFn } from '@angular/forms';
+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';
import { COMMA, ENTER } from '@angular/cdk/keycodes';
@@ -74,8 +87,12 @@ export interface PeriodicElement {
styleUrls: ['./create-feature.component.scss'],
providers: [CreateFeatureComponentStore],
})
-export class CreateFeatureComponent extends BaseComponent implements OnDestroy, OnInit {
+export class CreateFeatureComponent extends BaseComponent implements OnDestroy, OnInit, AfterViewInit {
@ViewChildren('stepper') stepper: QueryList
;
+ @ViewChild('blockNameStepContent', { read: ElementRef }) blockNameStepContent: ElementRef;
+ @ViewChild('authorizationStepContent', { read: ElementRef }) authorizationStepContent: ElementRef;
+ @ViewChild('configureMethodDialogTemplate', { read: TemplateRef })
+ configureMethodDialogTemplateRef: TemplateRef;
public taxes: any[] = [];
public createPlanForm: any;
public taxConfigData: any;
@@ -132,11 +149,15 @@ export class CreateFeatureComponent extends BaseComponent implements OnDestroy,
public paymentDetailsById$: Observable = this.componentStore.paymentDetailsById$;
public updatePaymentDetails$: Observable = this.componentStore.updatePaymentDetails$;
public webhookEvents$: Observable = this.componentStore.webhookEvents$;
+ public uploadLogo$: Observable = this.componentStore.uploadLogo$;
public isEditMode = false;
public previewInputPosition: 'top' | 'bottom' = 'top';
public selectedServiceIndex = 0;
public selectedSubscriptionServiceIndex = -2;
+ /** Duplicate of service form used inside configure method dialog; patched back to serviceForm on close */
+ public configureMethodDialogForm: ServiceFormGroup | null = null;
+
public selectedMethod = new BehaviorSubject(null);
public featureId: number = null;
public nameFieldEditMode = false;
@@ -150,6 +171,12 @@ export class CreateFeatureComponent extends BaseComponent implements OnDestroy,
public featureFieldType = FeatureFieldType;
public proxyAuthScript = ProxyAuthScript(environment.proxyServer);
+ public configureMethodsTableColumns: string[] = ['method', 'toggle', 'configure'];
+
+ /** Table data for Configure Method step (create block). Set when method loads so dataSource is stable and toggle binding works. */
+ public configureMethodsTableData: { name: string; index: number }[] = [];
+
+ public configureMethodDialog: MatDialogRef;
// Chip list
public chipListSeparatorKeysCodes: number[] = [ENTER, COMMA];
public chipListValues: { [key: string]: Set } = {};
@@ -158,8 +185,14 @@ export class CreateFeatureComponent extends BaseComponent implements OnDestroy,
// File
public fileValues: { [key: string]: FileList } = {};
+ /** Logo input: 'url' = enter URL, 'file' = upload file */
+ public logoInputMode: 'url' | 'file' = 'url';
+
+ public isLogoUploading = false;
+
// Options cache for select fields
private optionsCache: { [key: string]: any[] } = {};
+ public logoUrl: string = null;
public featureForm = new FormGroup({
primaryDetails: new FormGroup({
@@ -170,8 +203,9 @@ export class CreateFeatureComponent extends BaseComponent implements OnDestroy,
CustomValidators.noStartEndSpaces,
Validators.maxLength(60),
]),
- feature_id: new FormControl(null, [Validators.required]),
+ feature_id: new FormControl(1, [Validators.required]),
method_id: new FormControl(null, [Validators.required]),
+ redirect_url: new FormControl(null, [Validators.required, Validators.pattern(URL_REGEX)]),
}),
serviceDetails: new FormArray([]),
planDetails: new FormArray([]),
@@ -181,22 +215,17 @@ export class CreateFeatureComponent extends BaseComponent implements OnDestroy,
planSelected: new FormControl(null, [Validators.required]),
}),
authorizationDetails: new FormGroup({
- session_time: new FormControl(null, [
+ session_time: new FormControl(86400, [
Validators.required,
Validators.pattern(ONLY_INTEGER_REGEX),
Validators.min(60),
Validators.max(999999999),
]),
- authorizationKey: new FormControl(null, [
+ authorizationKey: new FormControl('Authorization', [
Validators.required,
CustomValidators.minLengthThreeWithoutSpace,
]),
- theme: new FormControl('system', []),
- version: new FormControl('v1', []),
- allowNewUserRegistration: new FormControl(false, []),
encryptionKey: new FormControl(null, []),
- redirect_url: new FormControl(null, [Validators.required, Validators.pattern(URL_REGEX)]),
- showSocialLoginIcons: new FormControl(false, []),
blockNewUserSignUps: new FormControl(false, []),
}),
webhookDetails: new FormGroup({
@@ -204,6 +233,21 @@ export class CreateFeatureComponent extends BaseComponent implements OnDestroy,
method: new FormControl('POST', [Validators.required]),
triggerEvents: new FormControl([], []),
}),
+ brandingDetails: new FormGroup({
+ icons: new FormControl(false, []),
+ logo_url: new FormControl(null, []),
+ light_theme_primary_color: new FormControl('#000000', []),
+ dark_theme_primary_color: new FormControl('#ffffff', []),
+ button_color: new FormControl('#19E6CE', []),
+ button_hover_color: new FormControl('#19E6CE', []),
+ button_text_color: new FormControl('#000000', []),
+ border_radius: new FormControl('small', []),
+ title: new FormControl('Sign in to your account', []),
+ sign_up_button_text: new FormControl('Create an account', [Validators.required]),
+ create_account_link: new FormControl(true, []),
+ theme: new FormControl('system', []),
+ version: new FormControl('v2', []),
+ }),
// New form controls for conditional steps
});
public demoDiv$: Observable = of(null);
@@ -229,6 +273,8 @@ export class CreateFeatureComponent extends BaseComponent implements OnDestroy,
this.getFeatureDetalis();
} else {
this.getFeatureType();
+ this.featureForm.get('brandingDetails.light_theme_primary_color')?.setValue('#000000');
+ this.featureForm.get('brandingDetails.dark_theme_primary_color')?.setValue('#ffffff');
}
});
@@ -283,10 +329,6 @@ export class CreateFeatureComponent extends BaseComponent implements OnDestroy,
authorizationDetails: {
session_time: feature.session_time,
authorizationKey: feature.authorization_format.key,
- theme: feature.ui_preferences?.theme || 'system',
- version: feature.ui_preferences?.version || 'v1',
- showSocialLoginIcons: feature.ui_preferences?.icons || false,
- allowNewUserRegistration: feature.ui_preferences?.create_account_link || false,
encryptionKey: feature.encryption_key,
blockNewUserSignUps: feature.block_registration || false,
},
@@ -295,10 +337,26 @@ export class CreateFeatureComponent extends BaseComponent implements OnDestroy,
method: feature.webhook?.method,
triggerEvents: feature.trigger_events || feature.webhook_events || [],
},
+ brandingDetails: {
+ logo_url: feature.ui_preferences?.logo_url,
+ light_theme_primary_color: feature.ui_preferences?.light_theme_primary_color,
+ dark_theme_primary_color: feature.ui_preferences?.dark_theme_primary_color,
+ button_color: feature.ui_preferences?.button_color,
+ button_hover_color: feature.ui_preferences?.button_hover_color,
+ button_text_color:
+ feature.ui_preferences?.button_text_color ?? feature.ui_preferences?.text_color,
+ border_radius: feature.ui_preferences?.border_radius,
+ title: feature.ui_preferences?.title,
+ sign_up_button_text: feature.ui_preferences?.sign_up_button_text ?? 'Create an account',
+ icons: feature.ui_preferences?.icons ?? false,
+ create_account_link: feature.ui_preferences?.create_account_link ?? true,
+ theme: feature.ui_preferences?.theme || 'system',
+ version: feature.ui_preferences?.version || 'v1',
+ },
});
// Clear redirect_url validators in edit mode since the field is hidden
- this.featureForm.get('authorizationDetails.redirect_url')?.clearValidators();
- this.featureForm.get('authorizationDetails.redirect_url')?.updateValueAndValidity();
+ this.featureForm.get('primaryDetails.redirect_url')?.clearValidators();
+ this.featureForm.get('primaryDetails.redirect_url')?.updateValueAndValidity();
this.previewInputPosition = feature.ui_preferences?.input_fields || 'top';
});
});
@@ -326,7 +384,12 @@ export class CreateFeatureComponent extends BaseComponent implements OnDestroy,
configurations: new FormGroup({}),
createPlanForm: new FormGroup({}),
chargesForm: new FormGroup({}),
- is_enable: new FormControl(this.isEditMode ? serviceValues?.is_enable : true),
+ is_enable: new FormControl(
+ this.isEditMode
+ ? serviceValues?.is_enable
+ : FeatureServiceIds.GoogleAuthentication === service.service_id ||
+ FeatureServiceIds.Msg91OtpService === service.service_id
+ ),
});
if (service.requirements) {
Object.entries(service.requirements).forEach(([key, config]) => {
@@ -352,6 +415,15 @@ export class CreateFeatureComponent extends BaseComponent implements OnDestroy,
}
this.featureForm.controls.serviceDetails.push(serviceFormGroup);
});
+ this.configureMethodsTableData = method.method_services.map((s, i) => ({ name: s.name, index: i }));
+ // After building service details for authorization block, sync redirect_url into all service redirect_uri fields
+ if (
+ !this.isEditMode &&
+ this.featureForm.get('primaryDetails.feature_id')?.value === 1 &&
+ this.featureForm.get('primaryDetails.redirect_url')?.value
+ ) {
+ this.setRedirectUrlInServiceDetails();
+ }
});
this.createUpdateObject$.pipe(filter(Boolean), takeUntil(this.destroy$)).subscribe((obj) => {
this.proxyAuthScript = ProxyAuthScript(
@@ -376,6 +448,8 @@ export class CreateFeatureComponent extends BaseComponent implements OnDestroy,
this.stepper?.first?.next();
}, 10);
}
+ this.featureId = obj.id;
+ this.demoDiv$ = of(``);
});
this.createBillableMetric$.pipe(filter(Boolean), takeUntil(this.destroy$)).subscribe((metric) => {
@@ -502,14 +576,110 @@ export class CreateFeatureComponent extends BaseComponent implements OnDestroy,
}
}
});
+ this.uploadLogo$.pipe(filter(Boolean), takeUntil(this.destroy$)).subscribe((data) => {
+ const url = data?.logo_url ?? data?.url;
+ if (url) {
+ // this.featureForm.get('brandingDetails.logo_url')?.setValue(url);
+ this.logoUrl = url;
+ }
+ this.isLogoUploading = false;
+ this.cdr.markForCheck();
+ });
+ }
+
+ public validateFirstStepAndNext(nameControl: AbstractControl): void {
+ nameControl.markAllAsTouched();
+ const featureId = this.featureForm.get('primaryDetails.feature_id')?.value;
+ if (featureId === 1) {
+ this.featureForm.get('primaryDetails.redirect_url')?.markAllAsTouched();
+ }
+ if (nameControl.invalid) {
+ return;
+ }
+ if (featureId === 1) {
+ const redirectUrlControl = this.featureForm.get('primaryDetails.redirect_url');
+ if (redirectUrlControl?.invalid) {
+ return;
+ }
+ // Sync redirect_url into all service redirect_uri fields before showing Configure Method step
+ this.setRedirectUrlInServiceDetails();
+ }
+ this.stepper?.first?.next();
+ }
+
+ public get validateFirstStep(): boolean {
+ const nameControl = this.featureForm.get('primaryDetails.name');
+ const featureId = this.featureForm.get('primaryDetails.feature_id')?.value;
+ const redirectUrlControl = this.featureForm.get('primaryDetails.redirect_url');
+ if (nameControl.invalid || redirectUrlControl?.invalid) {
+ return false;
+ }
+ if (featureId === 1) {
+ const redirectUrlControl = this.featureForm.get('primaryDetails.redirect_url');
+ if (redirectUrlControl?.invalid) {
+ return false;
+ }
+ // Sync redirect_url into all service redirect_uri fields before showing Configure Method step
+ this.setRedirectUrlInServiceDetails();
+ }
+ return true;
+ }
+
+ /** Default primary color: black in light theme, white in dark theme */
+ public getDefaultPrimaryColor(): string {
+ return typeof localStorage !== 'undefined' && localStorage.getItem('selected-theme') === 'dark-theme'
+ ? '#ffffff'
+ : '#000000';
+ }
+
+ /** Returns the effective primary color based on the selected branding theme */
+ public getEffectivePrimaryColor(): string {
+ const theme = this.featureForm.get('brandingDetails.theme')?.value;
+ const isDark =
+ theme === 'dark' ||
+ (theme === 'system' &&
+ typeof localStorage !== 'undefined' &&
+ localStorage.getItem('selected-theme') === 'dark-theme');
+ if (isDark) {
+ return this.featureForm.get('brandingDetails.dark_theme_primary_color')?.value || '#ffffff';
+ }
+ return this.featureForm.get('brandingDetails.light_theme_primary_color')?.value || '#000000';
+ }
+
+ public getBrandingBorderRadiusValue(): string {
+ const v = this.featureForm.get('brandingDetails.border_radius')?.value;
+ switch (v) {
+ case 'none':
+ return '0';
+ case 'small':
+ return '4px';
+ case 'medium':
+ return '8px';
+ case 'large':
+ return '12px';
+ default:
+ return '8px';
+ }
+ }
+
+ public onBrandingLogoFileSelected(event: Event): void {
+ const input = event.target as HTMLInputElement;
+ const file = input?.files?.[0];
+ if (!file || !this.featureId) return;
+ input.value = '';
+ const formData = new FormData();
+ formData.append('logo', file);
+ this.isLogoUploading = true;
+ this.cdr.markForCheck();
+ this.componentStore.uploadLogo({ id: this.featureId, formData });
}
public setRedirectUrlInServiceDetails(): void {
const serviceDetailsForm = this.featureForm.controls.serviceDetails;
- const redirectUrl = this.featureForm.controls.authorizationDetails.value.redirect_url;
+ const redirectUrl = this.featureForm.controls.primaryDetails.value.redirect_url;
serviceDetailsForm.controls.forEach((formGroup) => {
const redirectUrlControl = formGroup.controls.configurations.controls['redirect_uri'] as FormControl;
- if (redirectUrlControl) {
+ if (redirectUrlControl && !redirectUrlControl.value) {
redirectUrlControl.setValue(redirectUrl);
formGroup.markAsDirty();
}
@@ -531,10 +701,13 @@ export class CreateFeatureComponent extends BaseComponent implements OnDestroy,
},
session_time: featureFormData.authorizationDetails.session_time,
extra_configurations: {
- theme: featureFormData.authorizationDetails.theme || 'system',
- allowNewUserRegistration: featureFormData.authorizationDetails.allowNewUserRegistration || false,
+ theme: featureFormData.brandingDetails.theme,
+ create_account_link: featureFormData.brandingDetails.create_account_link || false,
},
services: this.getServicePayload(selectedMethod),
+ ui_preferences: {
+ ...featureFormData.brandingDetails,
+ },
};
// Added setTimeout because payload creation might contain promises
setTimeout(() => {
@@ -543,10 +716,12 @@ export class CreateFeatureComponent extends BaseComponent implements OnDestroy,
}
}
- public updateFeature(type: 'name' | 'service' | 'authorization' | 'webhook') {
+ public updateFeature(type: 'name' | 'service' | 'authorization' | 'branding' | 'webhook') {
let payload;
const selectedMethod = cloneDeep(this.selectedMethod.getValue());
const featureDetails: IFeatureDetails = this.getValueFromObservable(this.featureDetails$);
+ const brandingDetailsForm = this.featureForm.controls.brandingDetails;
+
switch (type) {
case 'name':
const primaryDetailsForm = this.featureForm.controls.primaryDetails;
@@ -564,8 +739,8 @@ export class CreateFeatureComponent extends BaseComponent implements OnDestroy,
if (authorizationDetailsForm.valid) {
payload = {
extra_configurations: {
- theme: authorizationDetailsForm.value.theme,
- create_account_link: authorizationDetailsForm.value.allowNewUserRegistration || false,
+ theme: brandingDetailsForm.controls.theme.value,
+ create_account_link: brandingDetailsForm.controls.create_account_link.value || false,
default_role: {
name: 'Owner',
value: 1,
@@ -577,13 +752,13 @@ export class CreateFeatureComponent extends BaseComponent implements OnDestroy,
...featureDetails.authorization_format,
key: authorizationDetailsForm.value.authorizationKey,
},
- ui_preferences: {
- theme: authorizationDetailsForm.value.theme,
- create_account_link: authorizationDetailsForm.value.allowNewUserRegistration || false,
- icons: authorizationDetailsForm.value.showSocialLoginIcons || false,
- version: authorizationDetailsForm.value.version,
- input_fields: this.previewInputPosition,
- },
+ // ui_preferences: {
+ // theme: brandingDetailsForm.controls.theme.value || 'system',
+ // create_account_link: brandingDetailsForm.controls.create_account_link.value || false,
+ // icons: brandingDetailsForm.controls.icons.value || false,
+ // version: brandingDetailsForm.controls.version.value || 'v1',
+ // input_fields: this.previewInputPosition,
+ // },
session_time: authorizationDetailsForm.value.session_time,
};
} else {
@@ -601,6 +776,25 @@ export class CreateFeatureComponent extends BaseComponent implements OnDestroy,
return;
}
break;
+ case 'branding':
+ if (brandingDetailsForm.valid) {
+ const existingUiPrefs = (featureDetails as unknown as Record)?.ui_preferences as
+ | Record
+ | undefined;
+ const { primary_color: _removed, ...cleanedUiPrefs } = existingUiPrefs || ({} as any);
+ payload = {
+ ui_preferences: {
+ ...cleanedUiPrefs,
+ ...brandingDetailsForm.value,
+ input_fields: this.previewInputPosition,
+ logo_url: this.logoInputMode === 'file' ? this.logoUrl : brandingDetailsForm.value.logo_url,
+ },
+ };
+ } else {
+ brandingDetailsForm.markAllAsTouched();
+ return;
+ }
+ break;
case 'webhook':
const webhookDetailsForm = this.featureForm.controls.webhookDetails;
@@ -663,6 +857,51 @@ export class CreateFeatureComponent extends BaseComponent implements OnDestroy,
if (!this.isEditMode && event?.previouslySelectedIndex === 0) {
this.getServiceMethods(this.featureForm.value.primaryDetails.feature_id);
}
+ // When entering Block Name step (index 0), focus the name input if empty
+ if (!this.isEditMode && event?.selectedIndex === 0) {
+ setTimeout(() => this.focusBlockNameInputIfEmpty(), 0);
+ }
+ // When entering Authorization Setup step (index 3), focus session time input if empty
+ if (
+ !this.isEditMode &&
+ this.featureForm.get('primaryDetails.feature_id')?.value === 1 &&
+ event?.selectedIndex === 3
+ ) {
+ setTimeout(() => this.focusSessionTimeInputIfEmpty(), 0);
+ }
+ // When entering Configure Method (step 1) for authorization block, ensure redirect_url is synced to all service redirect_uri fields
+ if (
+ !this.isEditMode &&
+ this.featureForm.get('primaryDetails.feature_id')?.value === 1 &&
+ event?.selectedIndex === 1
+ ) {
+ this.setRedirectUrlInServiceDetails();
+ }
+ }
+
+ public ngAfterViewInit(): void {
+ if (!this.isEditMode && this.stepper?.first?.selectedIndex === 0) {
+ setTimeout(() => this.focusBlockNameInputIfEmpty(), 100);
+ }
+ }
+
+ private focusBlockNameInputIfEmpty(): void {
+ const nameValue = this.featureForm.get('primaryDetails.name')?.value;
+ if (nameValue != null && String(nameValue).trim() !== '') {
+ return;
+ }
+ this.blockNameStepContent?.nativeElement?.querySelector('input')?.focus();
+ }
+
+ private focusSessionTimeInputIfEmpty(): void {
+ const sessionTimeValue = this.featureForm.get('authorizationDetails.session_time')?.value;
+ const hasValue =
+ sessionTimeValue != null &&
+ (typeof sessionTimeValue === 'string' ? String(sessionTimeValue).trim() !== '' : true);
+ if (hasValue) {
+ return;
+ }
+ this.authorizationStepContent?.nativeElement?.querySelector('input')?.focus();
}
public ngOnDestroy(): void {
@@ -801,8 +1040,13 @@ export class CreateFeatureComponent extends BaseComponent implements OnDestroy,
this.featureForm.controls.planDetails.push(planFormGroup);
}
- public resetFormGroup(formGroup: FormGroup, index: number): void {
+ public resetFormGroup(formGroup: FormGroup | null | undefined, index: number): void {
+ if (!formGroup) return;
+ const isEnableValue = formGroup.get('is_enable')?.value;
formGroup.reset();
+ if (formGroup.get('is_enable') && isEnableValue !== undefined) {
+ formGroup.get('is_enable')?.setValue(isEnableValue);
+ }
Object.keys(this.chipListValues)
.filter((key) => +key.split('_')[1] === index)
.forEach((key) => (this.chipListValues[key] = new Set(this.chipListReadOnlyValues[key])));
@@ -811,6 +1055,36 @@ export class CreateFeatureComponent extends BaseComponent implements OnDestroy,
.forEach((key) => (this.fileValues[key] = null));
}
+ /** Deep-clone a FormGroup (and nested FormGroups/FormArrays/FormControls) for use in dialog. Copies values and validators. */
+ private cloneFormGroup(source: FormGroup): FormGroup {
+ const clone = new FormGroup({});
+ Object.keys(source.controls).forEach((key) => {
+ const control = source.get(key);
+ if (control instanceof FormGroup) {
+ clone.addControl(key, this.cloneFormGroup(control));
+ } else if (control instanceof FormArray) {
+ clone.addControl(
+ key,
+ new FormArray(
+ control.controls.map((c) =>
+ c instanceof FormGroup ? this.cloneFormGroup(c) : this.cloneFormControl(c as FormControl)
+ )
+ )
+ );
+ } else {
+ clone.addControl(key, this.cloneFormControl(control as FormControl));
+ }
+ });
+ return clone;
+ }
+
+ private cloneFormControl(source: FormControl): FormControl {
+ return new FormControl(source.value, {
+ validators: source.validator,
+ asyncValidators: source.asyncValidator,
+ });
+ }
+
public updateChipListValues(
operation: 'add' | 'delete',
chipListKey: string,
@@ -1733,4 +2007,50 @@ export class CreateFeatureComponent extends BaseComponent implements OnDestroy,
this.http.post(url, sampleResponse).pipe(takeUntil(this.destroy$)).subscribe();
this.toast.success('Sample response sent successfully');
}
+
+ /** Reset only the configure dialog form (duplicate), preserving is_enable. Does not touch chipListValues/fileValues. */
+ public resetConfigureDialogForm(): void {
+ if (!this.configureMethodDialogForm) return;
+ const isEnableValue = this.configureMethodDialogForm.get('is_enable')?.value;
+ this.configureMethodDialogForm.reset();
+ if (this.configureMethodDialogForm.get('is_enable') && isEnableValue !== undefined) {
+ this.configureMethodDialogForm.get('is_enable')?.setValue(isEnableValue);
+ }
+ }
+
+ public editService(index: number): void {
+ this.selectedServiceIndex = index;
+ const serviceDetailsArray = this.featureForm.get('serviceDetails') as FormArray;
+ const sourceForm = serviceDetailsArray?.at(index) as ServiceFormGroup | null;
+ if (!sourceForm) return;
+ this.configureMethodDialogForm = this.cloneFormGroup(sourceForm) as ServiceFormGroup;
+ this.configureMethodDialog = this.dialog.open(this.configureMethodDialogTemplateRef);
+ this.configureMethodDialog
+ .afterClosed()
+ .pipe(takeUntil(this.destroy$))
+ .subscribe((res) => {
+ if (res) {
+ if (this.configureMethodDialogForm) {
+ const targetForm = serviceDetailsArray?.at(this.selectedServiceIndex) as FormGroup | null;
+ if (targetForm) {
+ targetForm.patchValue(this.configureMethodDialogForm.getRawValue());
+ }
+ this.configureMethodDialogForm = null;
+ }
+ }
+ });
+ }
+
+ public getSelectedServiceName(): string {
+ const method = this.selectedMethod.getValue();
+ return method?.method_services?.[this.selectedServiceIndex]?.name ?? 'Configure Service';
+ }
+
+ public closeDialog(): void {
+ if (this.configureMethodDialogForm?.valid) {
+ this.configureMethodDialog.close('true');
+ } else {
+ this.configureMethodDialogForm.markAllAsTouched();
+ }
+ }
}
diff --git a/apps/proxy/src/app/features/create-feature/create-feature.module.ts b/apps/proxy/src/app/features/create-feature/create-feature.module.ts
index f3d1b44b..554ff593 100644
--- a/apps/proxy/src/app/features/create-feature/create-feature.module.ts
+++ b/apps/proxy/src/app/features/create-feature/create-feature.module.ts
@@ -2,7 +2,7 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, Routes } from '@angular/router';
import { MatCardModule } from '@angular/material/card';
-import { ReactiveFormsModule } from '@angular/forms';
+import { ReactiveFormsModule, FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { UiLoaderModule } from '@proxy/ui/loader';
import { MatIconModule } from '@angular/material/icon';
@@ -26,6 +26,7 @@ import { CreatePlanDialogComponent } from './create-plan-dialog/create-plan-dial
import { CreateTaxDialogComponent } from './create-tax-dialog/create-tax-dialog.component';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatExpansionModule } from '@angular/material/expansion';
+import { MatRadioModule } from '@angular/material/radio';
const routes: Routes = [
{
@@ -41,6 +42,7 @@ const routes: Routes = [
CommonModule,
RouterModule.forChild(routes),
ReactiveFormsModule,
+ FormsModule,
MatInputModule,
MatFormFieldModule,
MatIconModule,
@@ -61,6 +63,7 @@ const routes: Routes = [
HttpClientModule,
MatTooltipModule,
MatExpansionModule,
+ MatRadioModule,
],
exports: [RouterModule],
})
diff --git a/apps/proxy/src/app/features/create-feature/create-feature.store.ts b/apps/proxy/src/app/features/create-feature/create-feature.store.ts
index 635292be..c0afe600 100644
--- a/apps/proxy/src/app/features/create-feature/create-feature.store.ts
+++ b/apps/proxy/src/app/features/create-feature/create-feature.store.ts
@@ -29,6 +29,7 @@ export interface ICreateFeatureInitialState {
paymentDetailsById: any;
updatePaymentDetails: any;
webhookEvents: any;
+ uploadLogo: any;
}
@Injectable()
@@ -58,6 +59,7 @@ export class CreateFeatureComponentStore extends ComponentStore = this.select((state) => state.updatePaymentDetails);
/** Selector for webhook events data */
readonly webhookEvents$: Observable = this.select((state) => state.webhookEvents);
+ /** Selector for upload logo data */
+ readonly uploadLogo$: Observable = this.select((state) => state.uploadLogo);
/** Get feature type data */
readonly getFeatureType = this.effect((data) => {
return data.pipe(
@@ -690,6 +694,31 @@ export class CreateFeatureComponentStore extends ComponentStore) => {
+ return data.pipe(
+ switchMap((req) => {
+ this.patchState({ isLoading: true, uploadLogo: null });
+ return this.service.uploadLogo(req.id, req.formData).pipe(
+ tapResponse(
+ (res: BaseResponse) => {
+ if (res?.hasError) {
+ this.showError(res.errors);
+ return this.patchState({ isLoading: false });
+ }
+ this.toast.success('Logo uploaded successfully');
+ return this.patchState({ isLoading: false, uploadLogo: res.data });
+ },
+ (error: any) => {
+ this.showError(error.errors);
+ this.patchState({ isLoading: false });
+ }
+ ),
+ catchError((err) => EMPTY)
+ );
+ })
+ );
+ });
+
private showError(error): void {
const errorMessage = errorResolver(error);
errorMessage.forEach((error) => {
diff --git a/apps/proxy/src/assets/scss/component/_buttons.scss b/apps/proxy/src/assets/scss/component/_buttons.scss
index a11162b5..71c8f767 100644
--- a/apps/proxy/src/assets/scss/component/_buttons.scss
+++ b/apps/proxy/src/assets/scss/component/_buttons.scss
@@ -297,4 +297,30 @@ button.mat-btn-md.mat-btn-wran:hover[disabled] {
background-color: var(--color-common-slate) !important;
}
}
+}
+.radio-button-custom-style {
+ .mat-radio-button {
+ .mat-radio-outer-circle {
+ border-color: var(--color-dark-accent) !important;
+ }
+
+ .mat-radio-inner-circle {
+ background-color: var(--color-dark-accent) !important;
+ }
+ }
+}
+.accent-color {
+ .material-icons {
+ &.mat-icon {
+ &.mat-icon-no-color {
+ color: var(--color-dark-accent) !important;
+ &:hover {
+ color: var(--color-dark-accent) !important;
+ }
+ }
+ }
+ }
+}
+.accent-bg-color {
+ background-color: var(--color-dark-accent) !important;
}
\ No newline at end of file
diff --git a/apps/proxy/src/styles.scss b/apps/proxy/src/styles.scss
index 060a77ff..2cf1da14 100644
--- a/apps/proxy/src/styles.scss
+++ b/apps/proxy/src/styles.scss
@@ -105,3 +105,9 @@ canvas {
width: 100%;
}
}
+
+// .block-feature-tabs {
+// .mat-tab-body-wrapper {
+// height: 100%;
+// }
+// }
diff --git a/libs/models/features-model/src/index.ts b/libs/models/features-model/src/index.ts
index 6003e565..46c8c956 100644
--- a/libs/models/features-model/src/index.ts
+++ b/libs/models/features-model/src/index.ts
@@ -98,6 +98,7 @@ export enum FeatureFieldType {
ReadFile = 'readFile',
Select = 'select',
TextArea = 'textarea',
+ Password = 'password',
}
export const ProxyAuthScript = (
@@ -132,6 +133,7 @@ export enum FeatureServiceIds {
Msg91OtpService = 6,
GoogleAuthentication = 7,
PasswordAuthentication = 9,
+ AppleAuthentication = 8,
}
export const ProxyUserManagementScript = (
baseUrl: string,
diff --git a/libs/services/proxy/features/src/lib/services-proxy-features.module.ts b/libs/services/proxy/features/src/lib/services-proxy-features.module.ts
index 3dcc3e01..f165e2b0 100644
--- a/libs/services/proxy/features/src/lib/services-proxy-features.module.ts
+++ b/libs/services/proxy/features/src/lib/services-proxy-features.module.ts
@@ -122,4 +122,10 @@ export class FeaturesService {
public getWebhookEvents(): Observable> {
return this.http.get>(FeaturesUrls.getWebhookEvents(this.baseURL));
}
+
+ public uploadLogo(id: string | number, formData: FormData): Observable> {
+ return this.http.post>(FeaturesUrls.uploadLogo(this.baseURL, id), formData, {
+ headers: {},
+ });
+ }
}
diff --git a/libs/urls/features-url/src/index.ts b/libs/urls/features-url/src/index.ts
index 373f99ee..c1fde911 100644
--- a/libs/urls/features-url/src/index.ts
+++ b/libs/urls/features-url/src/index.ts
@@ -20,4 +20,5 @@ export const FeaturesUrls = {
getPaymentDetailsFormById: (baseUrl, refid) => createUrl(baseUrl, `${refid}/paymentForm`),
updatePaymentDetails: (baseUrl, refid) => createUrl(baseUrl, `subscription/${refid}/updateCredentials`),
getWebhookEvents: (baseUrl) => createUrl(baseUrl, `getWebhookTriggerEvents`),
+ uploadLogo: (baseUrl, id) => createUrl(baseUrl, `features/${id}/upload-logo`),
};