Frontend IntegrationFrameworksAngular Integration

Angular Integration

Angular integration guide with basic (full-page and modal) and advanced widget methods for Sophi.

This page is organized in two levels:

  • Basic setup: copy-paste examples for full-page and modal usage
  • Advanced setup: optional runtime controls (sendToggleHistory, sendProductReply, triggerProductReply)

For GTM script loading and script-load failures, use SDK GTM troubleshooting.

Required in every integration

No matter which UI pattern you use (full page or modal), implement all three:

  1. Handle add_to_cart
  2. Handle send_to_checkout
  3. Call widget.updateUserCart(cart) when cart changes

Basic setup

1) Access object from window

const SophiWidgetClass = (window as any).SophiWebSDK?.SophiWidget;
if (!SophiWidgetClass) {
  throw new Error("Sophi SDK not loaded yet");
}

2) Initialize widget

const widget = new SophiWidgetClass();
await widget.init({
  apiKey: "YOUR_SOPHI_API_KEY",
  container: "#sophi-widget-container",
  environment: "production",
});

3) Full-page minimal example

import { AfterViewInit, Component, Input, OnChanges, OnDestroy, SimpleChanges } from "@angular/core";
import { Router } from "@angular/router";

type CartItem = { productId: string; variantId?: string; quantity: number };

@Component({
  selector: "app-sophi-full-page",
  template: `<div id="sophi-widget-container" style="width: 100%; height: 100vh;"></div>`,
})
export class SophiFullPageComponent implements AfterViewInit, OnChanges, OnDestroy {
  @Input() cartId = "";
  @Input() cartItems: CartItem[] = [];

  private widget: any = null;

  constructor(private readonly router: Router) {}

  async ngAfterViewInit(): Promise<void> {
    const SophiWidgetClass = (window as any).SophiWebSDK?.SophiWidget;
    if (!SophiWidgetClass) return;

    this.widget = new SophiWidgetClass();
    await this.widget.init({
      apiKey: "YOUR_SOPHI_API_KEY",
      container: "#sophi-widget-container",
      environment: "production",
      width: "100%",
      height: "100%",
    });

    this.widget.on("add_to_cart", async (data: { products: Array<{ productId: string; variantId?: string }> }) => {
      for (const product of data.products) {
        await addToCartInYourApp(product.productId, product.variantId);
      }
    });

    this.widget.on("send_to_checkout", () => {
      this.router.navigateByUrl("/checkout");
    });

    this.syncCart();
  }

  ngOnChanges(changes: SimpleChanges): void {
    if (!this.widget) return;
    if (changes["cartId"] || changes["cartItems"]) {
      this.syncCart();
    }
  }

  private syncCart(): void {
    this.widget.updateUserCart({
      cartId: this.cartId,
      items: this.cartItems,
    });
  }

  async ngOnDestroy(): Promise<void> {
    await this.widget?.destroy?.();
    this.widget = null;
  }
}

4) Modal minimal example

import { Component, Input, OnChanges, OnDestroy, SimpleChanges } from "@angular/core";
import { Router } from "@angular/router";

@Component({
  selector: "app-sophi-modal",
  template: `
    <button type="button" (click)="open()">Open assistant</button>
    <div *ngIf="isOpen" role="dialog" aria-modal="true">
      <button type="button" (click)="close()">Close</button>
      <div id="sophi-widget-modal-container" style="width: 100%; height: 70vh;"></div>
    </div>
  `,
})
export class SophiModalComponent implements OnChanges, OnDestroy {
  @Input() cartId = "";
  @Input() cartItems: Array<{ productId: string; variantId?: string; quantity: number }> = [];

  isOpen = false;
  private widget: any = null;

  constructor(private readonly router: Router) {}

  async open(): Promise<void> {
    this.isOpen = true;
    if (this.widget) return;
    const SophiWidgetClass = (window as any).SophiWebSDK?.SophiWidget;
    if (!SophiWidgetClass) return;

    this.widget = new SophiWidgetClass();
    await this.widget.init({
      apiKey: "YOUR_SOPHI_API_KEY",
      container: "#sophi-widget-modal-container",
      environment: "production",
      width: "100%",
      height: "100%",
    });

    this.widget.on("add_to_cart", async (data: { products: Array<{ productId: string; variantId?: string }> }) => {
      for (const product of data.products) {
        await addToCartInYourApp(product.productId, product.variantId);
      }
    });

    this.widget.on("send_to_checkout", () => {
      this.close();
      this.router.navigateByUrl("/checkout");
    });

    this.syncCart();
  }

  close(): void {
    this.isOpen = false;
  }

  ngOnChanges(changes: SimpleChanges): void {
    if (!this.widget) return;
    if (changes["cartId"] || changes["cartItems"]) {
      this.syncCart();
    }
  }

  private syncCart(): void {
    this.widget.updateUserCart({
      cartId: this.cartId,
      items: this.cartItems,
    });
  }

  async ngOnDestroy(): Promise<void> {
    await this.widget?.destroy?.();
    this.widget = null;
  }
}

5) Required events and cart sync wiring

widget.on("add_to_cart", async (data) => {
  for (const product of data.products) {
    await addToCartInYourApp(product.productId, product.variantId);
  }
});

widget.on("send_to_checkout", () => {
  router.navigateByUrl("/checkout");
});

widget.updateUserCart({
  cartId: cartId,
  items: cartItems,
});

Advanced setup

Call advanced methods only after ready event or widget.isReady():

widget.on("ready", () => {
  widget.sendToggleHistory();
  widget.triggerProductReply(product);
  widget.sendProductReply("Can you tell me more about this item?", product);
});
  • sendToggleHistory: open/close history panel
  • triggerProductReply: prefill product reply context
  • sendProductReply: send product-context query

To make the assistant available before the visitor accepts cookies, initialize with storageConsent: false and upgrade in place when consent is granted. See Cookie Consent Management for the full concept.

Initialize the widget once with the initial consent value, then call setStorageConsent(true) whenever your consent service emits a grant — do not destroy and re-create the widget, or the open chat is lost.

import { AfterViewInit, Component, OnDestroy } from "@angular/core";
import { Subscription } from "rxjs";
import { ConsentService } from "./consent.service"; // your own service

@Component({
  selector: "app-sophi-consent-aware",
  template: `<div id="sophi-widget-container" style="width: 100%; height: 100vh;"></div>`,
})
export class SophiConsentAwareComponent implements AfterViewInit, OnDestroy {
  private widget: any = null;
  private sub?: Subscription;

  constructor(private readonly consent: ConsentService) {}

  async ngAfterViewInit(): Promise<void> {
    const SophiWidgetClass = (window as any).SophiWebSDK?.SophiWidget;
    if (!SophiWidgetClass) return;

    this.widget = new SophiWidgetClass();
    await this.widget.init({
      apiKey: "YOUR_SOPHI_API_KEY",
      container: "#sophi-widget-container",
      environment: "production",
      width: "100%",
      height: "100%",
      storageConsent: this.consent.hasConsent(), // false → memory-only
    });

    // Upgrade the live widget in place when consent is granted.
    this.sub = this.consent.changes$.subscribe((granted: boolean) => {
      if (granted) {
        void this.widget?.setStorageConsent?.(true);
      }
    });
  }

  async ngOnDestroy(): Promise<void> {
    this.sub?.unsubscribe();
    await this.widget?.destroy?.();
    this.widget = null;
  }
}

Security

  • Keep YOUR_SOPHI_API_KEY as placeholder in docs
  • Do not put real secrets or internal endpoints into client code examples

On this page