Pushizo Docs
Rehberler

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

Service worker site kökünde olmalıdır: Vite/CRA için public/PushizoSDKWorker.js, Next.js için public/PushizoSDKWorker.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`);

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):

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;

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.

// 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;
}

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:

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

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

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

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).

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

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

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

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.

// 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;
}

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.

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

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

8. Bildirim tıklaması ve ön plan

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]);

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.

Bu sayfada