Guides

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();
  }
}