Angular Integration
Service-based integration patterns for Angular applications
Angular Integration
This guide covers Angular-specific patterns including services, RxJS observables, guards, and form integration. Angular’s dependency injection and reactive patterns work well with BotSigged’s real-time scoring.
Installation
npm install @botsigged/sdk
# or
yarn add @botsigged/sdk
# or
pnpm add @botsigged/sdk
Service Setup
BotSigged Service
Create a service to manage the SDK lifecycle:
// services/botsigged.service.ts
import { Injectable, OnDestroy } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { BotSigged, ScoreUpdate } from '@botsigged/sdk';
import { environment } from '../environments/environment';
@Injectable({
providedIn: 'root',
})
export class BotSiggedService implements OnDestroy {
private sdk: BotSigged | null = null;
private scoreSubject = new BehaviorSubject<ScoreUpdate | null>(null);
private connectedSubject = new BehaviorSubject<boolean>(false);
private readySubject = new BehaviorSubject<boolean>(false);
score$: Observable<ScoreUpdate | null> = this.scoreSubject.asObservable();
connected$: Observable<boolean> = this.connectedSubject.asObservable();
ready$: Observable<boolean> = this.readySubject.asObservable();
constructor() {
this.initialize();
}
private initialize(): void {
this.sdk = new BotSigged({
apiKey: environment.botsiggedApiKey,
autoStart: true,
onScoreUpdate: (score) => {
this.scoreSubject.next(score);
this.readySubject.next(true);
},
onConnectionChange: (connected) => {
this.connectedSubject.next(connected);
},
});
}
get currentScore(): ScoreUpdate | null {
return this.scoreSubject.value;
}
get isReady(): boolean {
return this.readySubject.value;
}
get isConnected(): boolean {
return this.connectedSubject.value;
}
async waitUntilReady(): Promise<{ score: number | null; timedOut: boolean }> {
const result = await this.sdk?.waitUntilReady();
return result ?? { score: null, timedOut: true };
}
getSessionId(): string | undefined {
return this.sdk?.getSessionId();
}
canSubmit(): { allowed: boolean; reason?: string; score?: number } {
return this.sdk?.canSubmit() ?? { allowed: false, reason: 'SDK not initialized' };
}
async stop(): Promise<void> {
await this.sdk?.stop();
}
async restart(): Promise<void> {
await this.stop();
this.initialize();
}
ngOnDestroy(): void {
this.sdk?.stop();
this.scoreSubject.complete();
this.connectedSubject.complete();
this.readySubject.complete();
}
}
Environment Configuration
// environments/environment.ts
export const environment = {
production: false,
botsiggedApiKey: 'your-api-key',
};
RxJS Operators
Custom Operators
Create reusable operators for common patterns:
// operators/botsigged.operators.ts
import { Observable, filter, map, take, timeout, catchError, of } from 'rxjs';
import { ScoreUpdate } from '@botsigged/sdk';
export function whenReady() {
return (source: Observable<ScoreUpdate | null>) =>
source.pipe(
filter((score): score is ScoreUpdate => score !== null),
take(1)
);
}
export function aboveThreshold(threshold: number) {
return (source: Observable<ScoreUpdate | null>) =>
source.pipe(
filter((score): score is ScoreUpdate => score !== null),
map((score) => score.bot_score > threshold)
);
}
export function withTimeout(ms: number) {
return (source: Observable<ScoreUpdate | null>) =>
source.pipe(
whenReady(),
timeout(ms),
catchError(() => of(null))
);
}
Usage:
import { whenReady, aboveThreshold } from './operators/botsigged.operators';
// Wait for first score
this.botSigged.score$.pipe(whenReady()).subscribe((score) => {
console.log('First score:', score.bot_score);
});
// React to high scores
this.botSigged.score$.pipe(aboveThreshold(70)).subscribe((isBot) => {
if (isBot) {
this.showWarning();
}
});
Form Protection
Protected Form Component
// components/protected-form/protected-form.component.ts
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { BotSiggedService } from '../../services/botsigged.service';
@Component({
selector: 'app-protected-form',
template: `
<form (ngSubmit)="onSubmit($event)">
<ng-content></ng-content>
</form>
`,
})
export class ProtectedFormComponent {
@Input() threshold = 70;
@Output() formSubmit = new EventEmitter<FormData>();
@Output() blocked = new EventEmitter<number>();
isSubmitting = false;
constructor(private botSigged: BotSiggedService) {}
async onSubmit(event: Event): Promise<void> {
event.preventDefault();
this.isSubmitting = true;
try {
const { score, timedOut } = await this.botSigged.waitUntilReady();
if (score && score > this.threshold) {
this.blocked.emit(score);
return;
}
const form = event.target as HTMLFormElement;
this.formSubmit.emit(new FormData(form));
} finally {
this.isSubmitting = false;
}
}
}
Usage:
<app-protected-form (formSubmit)="onSubmit($event)" (blocked)="onBlocked($event)">
<input name="email" type="email" required />
<button type="submit">Sign Up</button>
</app-protected-form>
Reactive Forms Integration
// components/signup/signup.component.ts
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { BotSiggedService } from '../../services/botsigged.service';
@Component({
selector: 'app-signup',
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<input formControlName="email" type="email" />
<div *ngIf="form.get('email')?.errors?.['email']">Invalid email</div>
<input formControlName="name" type="text" />
<button type="submit" [disabled]="isSubmitting || form.invalid">
{{ isSubmitting ? 'Submitting...' : 'Sign Up' }}
</button>
<div *ngIf="error" class="error">{{ error }}</div>
</form>
`,
})
export class SignupComponent {
form: FormGroup;
isSubmitting = false;
error: string | null = null;
constructor(
private fb: FormBuilder,
private botSigged: BotSiggedService
) {
this.form = this.fb.group({
email: ['', [Validators.required, Validators.email]],
name: ['', Validators.required],
});
}
async onSubmit(): Promise<void> {
if (this.form.invalid) return;
this.isSubmitting = true;
this.error = null;
try {
const { score } = await this.botSigged.waitUntilReady();
if (score && score > 70) {
this.error = 'Unable to verify your request. Please try again.';
return;
}
const response = await fetch('/api/signup', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-BotSigged-Session': this.botSigged.getSessionId() ?? '',
},
body: JSON.stringify(this.form.value),
});
if (!response.ok) {
throw new Error('Signup failed');
}
} catch (err) {
this.error = err instanceof Error ? err.message : 'An error occurred';
} finally {
this.isSubmitting = false;
}
}
}
Async Validator
Create a validator that checks bot score:
// validators/bot-check.validator.ts
import { Injectable } from '@angular/core';
import { AbstractControl, AsyncValidator, ValidationErrors } from '@angular/forms';
import { Observable, map, take } from 'rxjs';
import { BotSiggedService } from '../services/botsigged.service';
@Injectable({ providedIn: 'root' })
export class BotCheckValidator implements AsyncValidator {
constructor(private botSigged: BotSiggedService) {}
validate(control: AbstractControl): Observable<ValidationErrors | null> {
return this.botSigged.score$.pipe(
take(1),
map((score) => {
if (score && score.bot_score > 70) {
return { botDetected: true };
}
return null;
})
);
}
}
Usage:
constructor(
private fb: FormBuilder,
private botValidator: BotCheckValidator
) {
this.form = this.fb.group({
email: ['', [Validators.required], [this.botValidator]],
});
}
Route Guards
Bot Check Guard
Protect routes from suspected bots:
// guards/bot-check.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { BotSiggedService } from '../services/botsigged.service';
@Injectable({ providedIn: 'root' })
export class BotCheckGuard implements CanActivate {
constructor(
private botSigged: BotSiggedService,
private router: Router
) {}
async canActivate(): Promise<boolean> {
const { score, timedOut } = await this.botSigged.waitUntilReady();
// Allow if timed out (don't block legitimate users)
if (timedOut) return true;
if (score && score > 80) {
this.router.navigate(['/blocked']);
return false;
}
return true;
}
}
Apply to routes:
// app-routing.module.ts
const routes: Routes = [
{
path: 'checkout',
component: CheckoutComponent,
canActivate: [BotCheckGuard],
},
{
path: 'blocked',
component: BlockedComponent,
},
];
Functional Guard (Angular 15+)
// guards/bot-check.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { BotSiggedService } from '../services/botsigged.service';
export const botCheckGuard: CanActivateFn = async () => {
const botSigged = inject(BotSiggedService);
const router = inject(Router);
const { score, timedOut } = await botSigged.waitUntilReady();
if (timedOut) return true;
if (score && score > 80) {
return router.createUrlTree(['/blocked']);
}
return true;
};
Score Display Component
// components/bot-score/bot-score.component.ts
import { Component } from '@angular/core';
import { BotSiggedService } from '../../services/botsigged.service';
@Component({
selector: 'app-bot-score',
template: `
<div class="score-indicator" [ngClass]="statusClass$ | async">
<ng-container *ngIf="(connected$ | async) === false">
Disconnected
</ng-container>
<ng-container *ngIf="(connected$ | async) && (ready$ | async) === false">
Analyzing...
</ng-container>
<ng-container *ngIf="(ready$ | async)">
<span>Score: {{ (score$ | async)?.bot_score ?? 0 }}</span>
<span>{{ (score$ | async)?.classification }}</span>
</ng-container>
</div>
`,
styles: [`
.score-indicator { padding: 0.5rem 1rem; border-radius: 4px; }
.human { background: #d4edda; }
.suspicious { background: #fff3cd; }
.bot { background: #f8d7da; }
.disconnected, .loading { background: #e2e3e5; }
`],
})
export class BotScoreComponent {
score$ = this.botSigged.score$;
connected$ = this.botSigged.connected$;
ready$ = this.botSigged.ready$;
statusClass$ = this.score$.pipe(
map((score) => {
if (!this.botSigged.isConnected) return 'disconnected';
if (!this.botSigged.isReady) return 'loading';
return score?.classification ?? 'unknown';
})
);
constructor(private botSigged: BotSiggedService) {}
}
HTTP Interceptor
Add session ID to all outgoing requests:
// interceptors/botsigged.interceptor.ts
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
import { Observable } from 'rxjs';
import { BotSiggedService } from '../services/botsigged.service';
@Injectable()
export class BotSiggedInterceptor implements HttpInterceptor {
constructor(private botSigged: BotSiggedService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const sessionId = this.botSigged.getSessionId();
if (sessionId) {
const cloned = req.clone({
setHeaders: {
'X-BotSigged-Session': sessionId,
},
});
return next.handle(cloned);
}
return next.handle(req);
}
}
Register the interceptor:
// app.module.ts
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { BotSiggedInterceptor } from './interceptors/botsigged.interceptor';
@NgModule({
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: BotSiggedInterceptor,
multi: true,
},
],
})
export class AppModule {}
Standalone Components (Angular 15+)
// components/protected-form.component.ts
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
import { BotSiggedService } from '../services/botsigged.service';
@Component({
selector: 'app-protected-form',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<input formControlName="email" />
<button [disabled]="isSubmitting">Submit</button>
</form>
`,
})
export class ProtectedFormComponent {
private fb = inject(FormBuilder);
private botSigged = inject(BotSiggedService);
form = this.fb.group({
email: ['', [Validators.required, Validators.email]],
});
isSubmitting = false;
async onSubmit() {
this.isSubmitting = true;
const { score } = await this.botSigged.waitUntilReady();
// ... handle submission
this.isSubmitting = false;
}
}
Signals (Angular 16+)
Using Angular signals for reactive state:
// services/botsigged-signals.service.ts
import { Injectable, signal, computed } from '@angular/core';
import { BotSigged, ScoreUpdate } from '@botsigged/sdk';
@Injectable({ providedIn: 'root' })
export class BotSiggedSignalsService {
private sdk: BotSigged | null = null;
score = signal<ScoreUpdate | null>(null);
isConnected = signal(false);
isReady = signal(false);
botScore = computed(() => this.score()?.bot_score ?? 0);
classification = computed(() => this.score()?.classification ?? 'unknown');
isBot = computed(() => this.botScore() > 70);
constructor() {
this.sdk = new BotSigged({
apiKey: environment.botsiggedApiKey,
onScoreUpdate: (s) => {
this.score.set(s);
this.isReady.set(true);
},
onConnectionChange: (c) => this.isConnected.set(c),
});
}
async waitUntilReady() {
return this.sdk?.waitUntilReady();
}
}
Usage in components:
@Component({
template: `
<p>Score: {{ botSigged.botScore() }}</p>
<p>Classification: {{ botSigged.classification() }}</p>
<button [disabled]="botSigged.isBot()">Submit</button>
`,
})
export class MyComponent {
botSigged = inject(BotSiggedSignalsService);
}
Troubleshooting
Service Not Initialized
Ensure the service is provided at the root level:
@Injectable({ providedIn: 'root' })
Memory Leaks
Always unsubscribe from observables:
export class MyComponent implements OnDestroy {
private destroy$ = new Subject<void>();
ngOnInit() {
this.botSigged.score$
.pipe(takeUntil(this.destroy$))
.subscribe((score) => {
// handle score
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
SSR (Angular Universal)
Skip SDK initialization on the server:
import { PLATFORM_ID, Inject } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
constructor(@Inject(PLATFORM_ID) private platformId: Object) {
if (isPlatformBrowser(this.platformId)) {
this.initialize();
}
}