PushizoDocs
Entegrasyonlar

React ile Pushizo Web SDK

@pushizo/web çatıdan bağımsız bir tarayıcı SDK'sıdır; React'te tek bir provider ile başlatılır ve bileşenler bir hook üzerinden SDK'ya ve abonelik durumuna erişir. Bu rehber Create React App / Vite ve Next.js (App…

@pushizo/web çatıdan bağımsız bir tarayıcı SDK'sıdır; React'te tek bir provider ile başlatılır ve bileşenler bir hook üzerinden SDK'ya ve abonelik durumuna erişir. Bu rehber Create React App / Vite ve Next.js (App Router) kurulumlarını, service worker yolunu, login/logout, tag/olay ve bildirim kutusu bileşenini kapsar. Çalışan örnek: examples/web-nextjs (components/pushizo-provider.tsx, components/engagement-panel.tsx).

1. Kurulum

npm install @pushizo/web
```text

Service worker site kökünde olmalıdır: Vite/CRA için `public/PushizoSDKWorker.js`, Next.js için `public/PushizoSDKWorker.js`.

```js
// public/PushizoSDKWorker.js — gerçek worker API sunucusundan yüklenir; SDK güncellemeleri siteye dokunmadan alınır.
const apiUrl = new URL(self.location.href).searchParams.get('apiUrl') || 'https://api.example.com';
importScripts(`${apiUrl.replace(/\/$/, '')}/sdk-web/PushizoSDKWorker.js`);
```text

Next.js'te dosyanın tüm siteyi kapsayabilmesi için `next.config.ts` içinde başlık verin (`examples/web-nextjs/next.config.ts`):

```ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  async headers() {
    return [
      {
        source: '/PushizoSDKWorker.js',
        headers: [
          { key: 'Service-Worker-Allowed', value: '/' },
          { key: 'Cache-Control', value: 'no-cache' },
        ],
      },
    ];
  },
};
export default nextConfig;
```text

Ortam değişkenleri: `VITE_HS_APP_ID` / `VITE_HS_API_URL` (Vite) ya da `NEXT_PUBLIC_HS_APP_ID` / `NEXT_PUBLIC_HS_API_URL` (Next.js). App ID gizli değildir; API anahtarı (`hs_live_…`) istemciye asla konmaz.

## 2. Provider ve hook

SDK yalnızca tarayıcıda çalışır; `init` bir `useEffect` içinde, SSR dışında çağrılır. Abonelik durumu `User.pushSubscription.addObserver` ile React state'ine yansıtılır.

```tsx
// src/pushizo/provider.tsx
'use client'; // Next.js App Router; Vite/CRA'da gereksiz

import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from 'react';
import Pushizo, { type PushizoSdk, type PushSubscriptionState } from '@pushizo/web';

interface PushizoContextValue {
  sdk: PushizoSdk;
  ready: boolean;
  error: string | null;
  subscription: PushSubscriptionState | null;
}

const PushizoContext = createContext<PushizoContextValue | null>(null);

export function PushizoProvider({ appId, apiUrl, children }: { appId: string; apiUrl: string; children: ReactNode }) {
  const [ready, setReady] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [subscription, setSubscription] = useState<PushSubscriptionState | null>(null);

  useEffect(() => {
    if (!appId) {
      setError('App ID tanımlı değil');
      return;
    }
    let cancelled = false;
    const unsubscribe = Pushizo.User.pushSubscription.addObserver((state) => setSubscription(state));
    Pushizo.init({
      appId,
      apiUrl,
      serviceWorkerPath: '/PushizoSDKWorker.js',
      allowLocalhostAsSecureOrigin: window.location.hostname === 'localhost', // yalnızca geliştirme
      logLevel: 'warn',
      bell: { enabled: true },
    })
      .then(() => {
        if (cancelled) return;
        setReady(true);
        setSubscription(Pushizo.User.pushSubscription.state);
      })
      .catch((err: unknown) => {
        if (!cancelled) setError(err instanceof Error ? err.message : String(err));
      });
    return () => {
      cancelled = true;
      unsubscribe();
    };
  }, [appId, apiUrl]);

  const value = useMemo<PushizoContextValue>(() => ({ sdk: Pushizo, ready, error, subscription }), [ready, error, subscription]);
  return <PushizoContext.Provider value={value}>{children}</PushizoContext.Provider>;
}

export function usePushizo(): PushizoContextValue {
  const ctx = useContext(PushizoContext);
  if (!ctx) throw new Error('usePushizo, PushizoProvider içinde kullanılmalı');
  return ctx;
}
```text

`init` idempotenttir: React StrictMode'un geliştirmede efekti iki kez çalıştırması sorun yaratmaz; ikinci çağrı ilk `init` sözünü (promise) döndürür. `cancelled` bayrağı yalnızca sökülmüş bileşene state yazılmasını engeller.

Next.js kök düzeni:

```tsx
// app/layout.tsx
import { PushizoProvider } from '@/pushizo/provider';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="tr">
      <body>
        <PushizoProvider appId={process.env.NEXT_PUBLIC_HS_APP_ID ?? ''} apiUrl={process.env.NEXT_PUBLIC_HS_API_URL ?? 'http://localhost:4000'}>
          {children}
        </PushizoProvider>
      </body>
    </html>
  );
}
```text

Vite: `main.tsx` içinde `<PushizoProvider appId={import.meta.env.VITE_HS_APP_ID} apiUrl={import.meta.env.VITE_HS_API_URL}>`.

## 3. İzin ve abonelik düğmesi

```tsx
import { usePushizo } from '@/pushizo/provider';

export function PushToggle() {
  const { sdk, ready, subscription } = usePushizo();
  if (!ready) return <p>SDK yükleniyor…</p>;

  const permission = sdk.Notifications.permission; // 'default' | 'granted' | 'denied' | 'unsupported'
  if (permission === 'unsupported') return <p>Bu tarayıcı web push desteklemiyor (HTTPS gerekir).</p>;
  if (permission === 'denied') return <p>Bildirimler tarayıcı ayarlarından engellenmiş.</p>;

  return subscription?.optedIn ? (
    <button type="button" onClick={() => void sdk.User.pushSubscription.optOut()}>Bildirimleri kapat</button>
  ) : (
    <button type="button" onClick={() => void sdk.Notifications.requestPermission()}>Bildirimlere izin ver</button>
  );
}
```text

`requestPermission()` izin + aboneliği birlikte yapar; `optIn()` gerekirse izin ister. Dashboard'da yapılandırılan slidedown/bell istemleri `init` seçenekleriyle de açılabilir (`slidedown`, `bell`, `promptOptions`).

## 4. Login / logout

Kimlik doğrulama durumunuz değiştiğinde SDK'yı eşleyin. Identity verification zorunluysa `identity_hash` sunucunuzdan gelir (`Pushizo.identityHash(sdkSecret, externalId)` — Node/PHP SDK).

```tsx
import { useEffect } from 'react';
import { usePushizo } from '@/pushizo/provider';

export function useSyncIdentity(user: { id: string; identityHash?: string } | null) {
  const { sdk, ready } = usePushizo();
  useEffect(() => {
    if (!ready) return;
    if (user) void sdk.login(user.id, user.identityHash);
    else if (sdk.User.externalId) void sdk.logout();
  }, [ready, sdk, user]);
}
```text

`login` anonim cihazı kullanıcıya bağlar (birden fazla cihaz aynı kullanıcıda birleşir); `logout` cihazı yeni bir anonim kullanıcıya taşır.

## 5. Tag, özellik ve olay

```tsx
const { sdk } = usePushizo();

sdk.User.addTags({ plan: 'pro', city: 'İzmir' }); // çevrimdışı kuyruk; ardışık işlemler birleştirilir
sdk.User.removeTag('plan');
sdk.User.setLanguage('tr');
sdk.Session.sendEvent('purchase', { sku: 'A1' }, 49.9); // olay + dönüşüm değeri
```text

Tag çağrıları senkron döner ve kuyruğa yazılır; `sdk.User.getTags()` yerel kopyayı verir. Olaylar in-app mesaj tetiklerini de (`event: purchase`) ateşler.

## 6. SPA rota değişimi (in-app `screen` tetiği)

In-app mesajların `screen` tetiği için rota değişiminde ekran adını bildirin.

```tsx
// Next.js App Router
'use client';
import { useEffect } from 'react';
import { usePathname } from 'next/navigation';
import { usePushizo } from '@/pushizo/provider';

export function ScreenTracker() {
  const { sdk, ready } = usePushizo();
  const pathname = usePathname();
  useEffect(() => {
    if (ready) sdk.InAppMessages.setScreen(pathname);
  }, [ready, sdk, pathname]);
  return null;
}
```text

React Router'da `useLocation().pathname` ile aynı yaklaşım. Ödeme gibi adımlarda `sdk.InAppMessages.paused = true` ile gösterimi durdurabilirsiniz.

## 7. Bildirim kutusu bileşeni

`add_to_inbox: true` ile gönderilen mesajlar `Inbox` API'siyle listelenir. Kullanıcı ve ona ait bir abonelik (push, yoksa e-posta/SMS) gerekir; aksi halde `Inbox.list` açıklayıcı bir hata fırlatır — boş/hata durumlarını gösterin.

```tsx
'use client';
import { useCallback, useEffect, useState } from 'react';
import type { InboxItem } from '@pushizo/web';
import { usePushizo } from '@/pushizo/provider';

export function InboxPanel() {
  const { sdk, ready } = usePushizo();
  const [items, setItems] = useState<InboxItem[]>([]);
  const [unread, setUnread] = useState(0);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const load = useCallback(() => {
    setLoading(true);
    sdk.Inbox.list({ limit: 20 })
      .then((page) => { setItems(page.items); setUnread(page.unreadCount); setError(null); })
      .catch((e: unknown) => setError(e instanceof Error ? e.message : String(e)))
      .finally(() => setLoading(false));
  }, [sdk]);

  useEffect(() => {
    if (!ready) return;
    load();
    return sdk.Inbox.addObserver((e) => setUnread(e.unreadCount)); // type: listed | read | read_all | archived | count
  }, [ready, sdk, load]);

  const markRead = (id: string) =>
    void sdk.Inbox.markRead(id).then((updated) => setItems((list) => list.map((i) => (i.id === id ? updated : i))));
  const archive = (id: string) => void sdk.Inbox.archive(id).then(() => setItems((list) => list.filter((i) => i.id !== id)));

  if (!ready) return null;
  return (
    <section aria-label="Bildirim kutusu">
      <header>
        <h2>Bildirimler ({unread} okunmamış)</h2>
        <button type="button" onClick={load} disabled={loading}>{loading ? 'Yükleniyor…' : 'Yenile'}</button>
        <button type="button" onClick={() => void sdk.Inbox.markAllRead().then(load)}>Tümünü okundu işaretle</button>
      </header>
      {error ? <p role="alert">{error}</p> : items.length === 0 ? <p>Bildirim yok.</p> : (
        <ul>
          {items.map((item) => (
            <li key={item.id} data-read={item.read}>
              <strong>{item.title ?? '(başlıksız)'}</strong>
              {item.body && <p>{item.body}</p>}
              {item.url && <a href={item.url}>Aç</a>}
              <button type="button" disabled={item.read} onClick={() => markRead(item.id)}>Okundu</button>
              <button type="button" onClick={() => archive(item.id)}>Arşivle</button>
            </li>
          ))}
        </ul>
      )}
    </section>
  );
}
```text

Sayfalama: `page.hasMore` iken `sdk.Inbox.list({ cursor: page.nextCursor! })`.

## 8. Bildirim tıklaması ve ön plan

```tsx
useEffect(() => {
  if (!ready) return;
  const offClick = sdk.Notifications.addClickListener(({ notification, result }) => {
    router.push(result.url ?? notification.url ?? '/'); // result.actionId: aksiyon düğmesi
  });
  const offForeground = sdk.Notifications.addForegroundLifecycleListener((e) => {
    if (document.visibilityState === 'visible') e.preventDefault(); // sayfa açıkken sistem bildirimi gösterme
  });
  return () => { offClick(); offForeground(); };
}, [ready, sdk]);
```text

Tıklama, service worker'ın sekmeyi öne getirmesinden sonra sayfaya iletilir; eşleştirme `notificationClickHandlerMatch` / `notificationClickHandlerAction` init seçenekleriyle ayarlanır.

## 9. Testler ve sorun giderme

- `init` yalnızca istemci bileşeninde: "window is not defined" hatası SSR'da `init` çağrıldığını gösterir.
- Geliştirmede HTTP `localhost` için `allowLocalhostAsSecureOrigin: true`; üretimde HTTPS zorunlu.
- Service worker 404: dosya `public/` kökünde değil ya da `serviceWorkerPath` yanlış.
- Abonelik oluşmuyor: Dashboard → **Kanallar → Web Push** etkin değil (`vapid_public_key` boş).
- Uçtan uca: dashboard **Abonelikler**'de kaydı **Test cihazı** yapın → **Mesajlar → Test push gönder** → **Mesaj İzleme**'de `received`/`opened`.
- iOS Safari için ana ekrana ekleme ve kullanıcı hareketi gereksinimi: [iOS web push](/docs/channels/ios-pwa-web-push).

Bu sayfada