PushizoDocs
Entegrasyonlar

Angular ile Pushizo Web SDK

@pushizo/web Angular'da bir servis içinde başlatılır (provideAppInitializer), abonelik durumu signal/BehaviorSubject ile bileşenlere akar. Bu rehber standalone bileşen mimarisini (Angular 17+) esas alır; NgModule…

@pushizo/web Angular'da bir servis içinde başlatılır (provideAppInitializer), abonelik durumu signal/BehaviorSubject ile bileşenlere akar. Bu rehber standalone bileşen mimarisini (Angular 17+) esas alır; NgModule tabanlı projelerde aynı servis APP_INITIALIZER ile kurulur.

1. Kurulum

npm install @pushizo/web
```text

Service worker site kökünden sunulmalıdır. `src/PushizoSDKWorker.js` dosyasını oluşturup `angular.json` `assets` listesine kök çıktıyla ekleyin:

```js
// src/PushizoSDKWorker.js
const apiUrl = new URL(self.location.href).searchParams.get('apiUrl') || 'https://api.example.com';
importScripts(`${apiUrl.replace(/\/$/, '')}/sdk-web/PushizoSDKWorker.js`);
```text

```json
"assets": [
  { "glob": "**/*", "input": "public" },
  { "glob": "PushizoSDKWorker.js", "input": "src", "output": "/" }
]
```text

Angular PWA (`@angular/pwa`, `ngsw-worker.js`) kullanıyorsanız iki service worker aynı kapsamda (`/`) kayıt olamaz. İki seçenek:

- Pushizo worker'ını **farklı kapsamda** kaydedin: `serviceWorkerScope: '/pushizo/'` (push aboneliği kapsamdan bağımsız çalışır; bildirim tıklaması yine `url`'yi açar). Kapsam dosya konumundan dar olduğundan ek başlık gerekmez.
- Ya da ngsw'yi kullanmayıp kendi worker'ınıza `importScripts` satırını ekleyin.

Ortam: `src/environments/environment.ts` içinde `hsAppId`, `hsApiUrl`.

## 2. Servis

```ts
// src/app/pushizo.service.ts
import { Injectable, PLATFORM_ID, inject, signal } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import Pushizo, { type PushizoSdk, type InitOptions, type PushSubscriptionState } from '@pushizo/web';
import { environment } from '../environments/environment';

@Injectable({ providedIn: 'root' })
export class PushizoService {
  readonly sdk: PushizoSdk = Pushizo;
  readonly ready = signal(false);
  readonly error = signal<string | null>(null);
  readonly subscription = signal<PushSubscriptionState | null>(null);
  private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));

  /** `provideAppInitializer` içinden çağrılır; SSR'da (Angular Universal) hiçbir şey yapmaz. */
  async init(options?: Partial<InitOptions>): Promise<void> {
    if (!this.isBrowser) return;
    this.sdk.User.pushSubscription.addObserver((state) => this.subscription.set(state));
    try {
      await this.sdk.init({
        appId: environment.hsAppId,
        apiUrl: environment.hsApiUrl,
        serviceWorkerPath: '/PushizoSDKWorker.js',
        allowLocalhostAsSecureOrigin: !environment.production,
        bell: { enabled: true },
        ...options,
      });
      this.ready.set(true);
      this.subscription.set(this.sdk.User.pushSubscription.state);
    } catch (err) {
      this.error.set(err instanceof Error ? err.message : String(err));
    }
  }

  get permission() {
    return this.ready() ? this.sdk.Notifications.permission : 'default';
  }

  requestPermission() {
    return this.sdk.Notifications.requestPermission();
  }

  login(externalId: string, identityHash?: string) {
    return this.sdk.login(externalId, identityHash);
  }

  logout() {
    return this.sdk.User.externalId ? this.sdk.logout() : Promise.resolve();
  }
}
```text

```ts
// src/app/app.config.ts
import { ApplicationConfig, provideAppInitializer, inject } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { PushizoService } from './pushizo.service';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    // Uygulama açılışını bloklamamak için init beklenmez; bileşenler `ready()` sinyalini izler.
    provideAppInitializer(() => {
      void inject(PushizoService).init();
    }),
  ],
};
```text

NgModule projelerinde: `{ provide: APP_INITIALIZER, useFactory: (hs: PushizoService) => () => { void hs.init(); }, deps: [PushizoService], multi: true }`.

## 3. İzin ve abonelik bileşeni

```ts
// src/app/push-toggle.component.ts
import { Component, inject } from '@angular/core';
import { PushizoService } from './pushizo.service';

@Component({
  selector: 'app-push-toggle',
  standalone: true,
  template: `
    @if (!hs.ready()) {
      <p>SDK yükleniyor…</p>
    } @else if (hs.permission === 'unsupported') {
      <p>Bu tarayıcı web push desteklemiyor (HTTPS gerekir).</p>
    } @else if (hs.permission === 'denied') {
      <p>Bildirimler tarayıcı ayarlarından engellenmiş.</p>
    } @else if (hs.subscription()?.optedIn) {
      <button type="button" (click)="hs.sdk.User.pushSubscription.optOut()">Bildirimleri kapat</button>
    } @else {
      <button type="button" (click)="hs.requestPermission()">Bildirimlere izin ver</button>
    }
  `,
})
export class PushToggleComponent {
  readonly hs = inject(PushizoService);
}
```text

## 4. Login / logout

Kimlik doğrulama servisinizin kullanıcı akışına bağlayın:

```ts
// src/app/auth.service.ts (ilgili bölüm)
import { effect, inject } from '@angular/core';
import { PushizoService } from './pushizo.service';

export class AuthService {
  private readonly hs = inject(PushizoService);
  readonly user = signal<{ id: string; identityHash?: string } | null>(null);

  constructor() {
    effect(() => {
      if (!this.hs.ready()) return;
      const user = this.user();
      if (user) void this.hs.login(user.id, user.identityHash); // identity_hash: HMAC-SHA256(sdk_secret, external_id), sunucuda üretilir
      else void this.hs.logout();
    });
  }
}
```text

## 5. Tag, olay ve rota takibi

```ts
hs.sdk.User.addTags({ plan: 'pro' });
hs.sdk.User.removeTag('plan');
hs.sdk.Session.sendEvent('purchase', { sku: 'A1' }, 49.9);
```text

In-app `screen` tetiği için rota değişimlerini bildirin:

```ts
// src/app/app.component.ts
import { Component, inject } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { filter } from 'rxjs';
import { PushizoService } from './pushizo.service';

export class AppComponent {
  private readonly hs = inject(PushizoService);
  constructor(router: Router) {
    router.events.pipe(filter((e): e is NavigationEnd => e instanceof NavigationEnd)).subscribe((e) => {
      if (this.hs.ready()) this.hs.sdk.InAppMessages.setScreen(e.urlAfterRedirects);
    });
  }
}
```text

## 6. Bildirim kutusu bileşeni

```ts
// src/app/inbox.component.ts
import { Component, DestroyRef, effect, inject, signal } from '@angular/core';
import type { InboxItem } from '@pushizo/web';
import { PushizoService } from './pushizo.service';

@Component({
  selector: 'app-inbox',
  standalone: true,
  template: `
    <section aria-label="Bildirim kutusu">
      <h2>Bildirimler ({{ unread() }} okunmamış)</h2>
      <button type="button" [disabled]="loading()" (click)="load()">{{ loading() ? 'Yükleniyor…' : 'Yenile' }}</button>
      <button type="button" (click)="markAllRead()">Tümünü okundu işaretle</button>
      @if (error()) {
        <p role="alert">{{ error() }}</p>
      } @else if (items().length === 0) {
        <p>Bildirim yok.</p>
      } @else {
        <ul>
          @for (item of items(); track item.id) {
            <li [attr.data-read]="item.read">
              <strong>{{ item.title ?? '(başlıksız)' }}</strong>
              @if (item.body) { <p>{{ item.body }}</p> }
              @if (item.url) { <a [href]="item.url">Aç</a> }
              <button type="button" [disabled]="item.read" (click)="markRead(item.id)">Okundu</button>
              <button type="button" (click)="archive(item.id)">Arşivle</button>
            </li>
          }
        </ul>
      }
    </section>
  `,
})
export class InboxComponent {
  private readonly hs = inject(PushizoService);
  readonly items = signal<InboxItem[]>([]);
  readonly unread = signal(0);
  readonly loading = signal(false);
  readonly error = signal<string | null>(null);

  constructor(destroyRef: DestroyRef) {
    effect(() => {
      if (!this.hs.ready()) return;
      void this.load();
      const stop = this.hs.sdk.Inbox.addObserver((e) => this.unread.set(e.unreadCount));
      destroyRef.onDestroy(stop);
    });
  }

  async load() {
    this.loading.set(true);
    try {
      const page = await this.hs.sdk.Inbox.list({ limit: 20 });
      this.items.set(page.items);
      this.unread.set(page.unreadCount);
      this.error.set(null);
    } catch (e) {
      this.error.set(e instanceof Error ? e.message : String(e)); // kullanıcı/abonelik yoksa açıklayıcı hata
    } finally {
      this.loading.set(false);
    }
  }
  async markRead(id: string) {
    const updated = await this.hs.sdk.Inbox.markRead(id);
    this.items.update((list) => list.map((i) => (i.id === id ? updated : i)));
  }
  async markAllRead() {
    await this.hs.sdk.Inbox.markAllRead();
    await this.load();
  }
  async archive(id: string) {
    await this.hs.sdk.Inbox.archive(id);
    this.items.update((list) => list.filter((i) => i.id !== id));
  }
}
```text

## 7. Bildirim tıklaması

```ts
// PushizoService.init sonrasında
const off = this.sdk.Notifications.addClickListener(({ notification, result }) => {
  void this.router.navigateByUrl(result.url ?? notification.url ?? '/');
});
// uygulama ömrü boyunca tutulur; gerekirse off() ile kaldırılır
```text

Angular `zone.js` kullanıyorsa SDK geri çağrıları zone dışında gelebilir; `signal` tabanlı durum güncellemeleri zone'dan bağımsız çalışır, `ChangeDetectorRef` gerekmez. Zone tabanlı şablon bağlamalarında `NgZone.run` içinde güncelleyin.

## 8. Sorun giderme

- `ReferenceError: window is not defined` (SSR): `init` yalnızca `isPlatformBrowser` doğruysa çağrılmalı.
- Service worker kapsam çakışması (ngsw): `serviceWorkerScope: '/pushizo/'` kullanın.
- `PushizoSDKWorker.js` 404: `angular.json` `assets` kaydı kök çıktıya (`"output": "/"`) yazmalı.
- Diğer belirtiler: [Web SDK sorun giderme](/docs/sdk/web#sorun-giderme); iOS Safari için [iOS web push](/docs/channels/ios-pwa-web-push).

Bu sayfada