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).MCP sunucusu: yapay zekâ asistanlarıyla Pushizo
Pushizo, Model Context Protocol (MCP) sunucusuyla Claude Desktop, Claude.ai ve diğer MCP istemcilerinin segment, kampanya, mesaj, şablon ve analitik verinizi okumasını ve onayınızla taslak kampanya/segment hazırlamasını…
Vue ile Pushizo Web SDK
@pushizo/web Vue 3'te bir plugin ile başlatılır; bileşenler usePushizo() composable'ı üzerinden SDK'ya, hazır olma durumuna ve abonelik durumuna (reaktif) erişir. Nuxt 3 için yalnızca istemci tarafında çalışan bir…