import { ChangeDetectionStrategy, Component, inject, DestroyRef, signal, ViewEncapsulation, OnInit } from '@angular/core'; import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { AbstractControl, FormBuilder, ReactiveFormsModule, ValidationErrors, Validators } from '@angular/forms'; import { Router, RouterLink } from '@angular/router'; import { BaseAuthService } from '../../service/auth.service'; import { BillingService } from '../../service/billing.service'; import { MatButtonModule } from '@angular/material/button'; import { MatFormFieldModule } from "@angular/material/form-field"; import { MatInputModule } from '@angular/material/input'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatSelectModule } from '@angular/material/select'; import { catchError, debounceTime, map, merge, of, switchMap, tap } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { PartnerRegistration, PublicPlan } from '../../model/appdata'; import { SAIL_GUI_CONFIG, SailGuiConfig, DEFAULT_CONFIG } from '../../config'; import { fromPriceLabel } from '../../util/money'; import { errorDetail } from '../../util/errors'; const passwordsMatch = (group: AbstractControl): ValidationErrors | null => group.get('Password')?.value === group.get('ConfirmPassword')?.value ? null : { passwordMismatch: true }; interface GeocodingResponse { results: { geometry: { location: { lat: number; lng: number } } }[]; status: string; } @Component({ selector: 'sail-register', templateUrl: './register_component.html', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [ ReactiveFormsModule, RouterLink, MatButtonModule, MatFormFieldModule, MatInputModule, MatCheckboxModule, MatSelectModule, ], }) export class RegisterComponent implements OnInit { private auth = inject(BaseAuthService); private billing = inject(BillingService); private fb = inject(FormBuilder); private http = inject(HttpClient); private router = inject(Router); protected readonly guiConfig: SailGuiConfig = inject(SAIL_GUI_CONFIG, {optional: true}) ?? DEFAULT_CONFIG; /** Geocoding is only active when a Google Maps key is configured; without * one the address-consent + verify step is skipped so registration still works. */ protected readonly geocodeEnabled = !!this.guiConfig.googleMapsApiKey; readonly registerError = signal(''); readonly plansError = signal(''); readonly plans = signal([]); readonly registerForm = this.fb.group({ FirstName: ['', Validators.required], LastName: ['', Validators.required], UserName: ['', Validators.required], Email: ['', [Validators.required, Validators.email]], Password: ['', [Validators.required, Validators.minLength(6)]], ConfirmPassword: ['', Validators.required], PartnerCaption: ['', Validators.required], Address: ['', Validators.required], City: ['', Validators.required], State: ['', Validators.required], Zipcode: ['', Validators.required], Country: ['', Validators.required], Phone: ['', Validators.required], DomainURL: ['', Validators.required], PlanID: ['FREE', Validators.required], Latitude: [null as number | null], Longitude: [null as number | null], consentLocation: [false, this.geocodeEnabled ? Validators.requiredTrue : []], }, { validators: passwordsMatch }); readonly geocodeStatus = signal<'idle' | 'loading' | 'success' | 'error'>('idle'); readonly geocodeError = signal(''); private consentLocation = toSignal( this.registerForm.controls.consentLocation.valueChanges, { initialValue: false } ); private destroyRef = inject(DestroyRef); private addressFields = ['Address', 'City', 'State', 'Zipcode', 'Country'] as const; protected getGoogleMapsApiKey(): string { return this.guiConfig.googleMapsApiKey ?? ''; } /** Indicative "from" price for a plan option, derived from its offers ('' = free/none). */ protected priceHint(plan: PublicPlan): string { return fromPriceLabel(plan.prices); } ngOnInit() { this.billing.listPlans().subscribe({ next: (list) => this.plans.set(list ?? []), error: () => this.plansError.set('Could not load subscription plans. Please reload the page.'), }); } constructor() { // Debounced + switchMapped so per-keystroke edits don't fan out billable // geocode calls and a stale response can't overwrite a newer address. merge( ...this.addressFields.map((f) => this.registerForm.controls[f].valueChanges), this.registerForm.controls.consentLocation.valueChanges, ).pipe( takeUntilDestroyed(this.destroyRef), tap(() => this.registerForm.patchValue({ Latitude: null, Longitude: null })), debounceTime(400), switchMap(() => this.geocode$()), ).subscribe(); } private geocode$() { if (!this.geocodeEnabled || !this.consentLocation()) { this.geocodeStatus.set('idle'); this.geocodeError.set(''); return of(null); } const { Address, City, State, Zipcode, Country } = this.registerForm.getRawValue(); if (!Address || !City || !State || !Zipcode || !Country) { this.geocodeStatus.set('idle'); this.geocodeError.set(''); return of(null); } this.geocodeStatus.set('loading'); this.geocodeError.set(''); const address = `${Address}, ${City}, ${State} ${Zipcode}, ${Country}`; const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=${this.getGoogleMapsApiKey()}`; return this.http.get(url).pipe( map((data) => { if (data.status !== 'OK' || data.results.length === 0) { throw new Error(`Geocoding failed: ${data.status}`); } return data.results[0].geometry.location; }), tap((coords) => { this.registerForm.patchValue({ Latitude: coords.lat, Longitude: coords.lng }); this.geocodeStatus.set('success'); }), catchError(() => { this.registerForm.patchValue({ Latitude: null, Longitude: null }); this.geocodeStatus.set('error'); this.geocodeError.set('Could not verify this address. Please check and try again.'); return of(null); }), ); } register() { if (!this.registerForm.valid) return; this.registerError.set(''); const formValue = this.registerForm.getRawValue(); const partnerReg: PartnerRegistration = { FirstName: formValue.FirstName!, LastName: formValue.LastName!, UserName: formValue.UserName!, Email: formValue.Email!, Password: formValue.Password!, PartnerCaption: formValue.PartnerCaption!, Address: formValue.Address!, City: formValue.City!, State: formValue.State!, Zipcode: formValue.Zipcode!, Country: formValue.Country!, Phone: formValue.Phone!, DomainURL: formValue.DomainURL!, Latitude: formValue.Latitude, Longitude: formValue.Longitude, PlanID: formValue.PlanID!, }; this.auth.register(partnerReg).subscribe({ // Pass the email along so the confirm page pre-fills it. next: () => this.router.navigate(['/confirm/register'], { queryParams: { email: formValue.Email } }), error: (err) => this.registerError.set(errorDetail(err, 'Registration failed. Please try again.')), }); } }