OneSignal'den Pushizo'ya göç
Pushizo, OneSignal'in kavram modelini (app, abonelik, kullanıcı/alias, tag, segment, şablon, journey) ve SDK yüzeyini (initialize → login → User.addTags → Notifications.requestPermission → Session.sendEvent) bilinçli…
Pushizo, OneSignal'in kavram modelini (app, abonelik, kullanıcı/alias, tag, segment, şablon, journey) ve SDK yüzeyini (initialize → login → User.addTags → Notifications.requestPermission → Session.sendEvent) bilinçli olarak korur. Bu rehber mevcut bir OneSignal uygulamasını kesintisiz taşımanın adımlarını, alan/uç eşlemelerini ve SDK değişikliklerini anlatır.
Araçlar: npm run migrate:onesignal (scripts/migrate-onesignal.ts), CSV içe aktarıcı (packages/db/src/data-transfer.ts → mapOneSignalRow), Postman koleksiyonu (api/postman_collection.json).
1. Genel plan
| Adım | Süre | Çıktı |
|---|---|---|
| 1. Hesap, organizasyon ve app oluşturma | 10 dk | app_… kimliği |
2. Kanal kimlik bilgileri (APNs .p8, FCM servis hesabı, VAPID, e-posta/SMS) | 30 dk | Kanallar active |
3. Player/abonelik göçü (migrate:onesignal → CSV → içe aktarma) | dosya boyutuna göre | Kullanıcılar, tag'ler, iOS/Android/e-posta/SMS abonelikleri |
| 4. Sunucu entegrasyonu (REST → Pushizo public API / sunucu SDK'ları) | 1–3 gün | Bildirim, kullanıcı, olay çağrıları |
| 5. İstemci SDK değişimi (Web/iOS/Android/Flutter, React Native/Capacitor/Cordova/.NET MAUI/Unity) | 1–2 gün | Yeni sürüm yayını |
| 6. Çift yazma dönemi ve kesinti | 2–4 hafta | OneSignal kapatılır |
2. Hesap ve app oluşturma
- Dashboard'da kayıt olun (
/register), e-posta doğrulaması ve isteğe bağlı MFA'yı tamamlayın. Her hesap bir organizasyon altında çalışır; organizasyon OneSignal'deki "hesap", app ise OneSignal app'inin karşılığıdır. - Başlangıç sihirbazı (
/<org>/getting-started) ile app'i oluşturun ya da public API'den:POST /v1/apps(org geneliadminanahtarı gerekir). - Organizasyon → API anahtarları'ndan bir sunucu anahtarı (
hs_live_…) üretin. Kapsamlar OneSignal REST API anahtarından daha incedir:notifications:write,users:read,users:write,events:write,segments:read,analytics:read,admin. Göç içinadminkapsamlı, IP izin listeli ve bitiş tarihli bir anahtar önerilir. - Kanallar sayfasında OneSignal'de kullandığınız kimlik bilgilerini aynen girin:
| OneSignal ayarı | Pushizo kanalı | Notlar |
|---|---|---|
Apple → .p8 anahtarı, Key ID, Team ID, Bundle ID | iOS Push (ios_push) | Ortam sandbox / production; aynı .p8 her iki tarafta çalışır. |
| Google → Firebase servis hesabı JSON | Android Push (android_push) | Aynı Firebase projesi kullanılır; cihaz token'ları geçerliliğini korur. |
| Web → Safari/Chrome ayarları | Web Push (web_push) | Pushizo kendi VAPID çiftini üretir; mevcut tarayıcı abonelikleri taşınamaz (bkz. §4.3). |
| E-posta (SendGrid vb.) | E-posta (email) | SMTP / Amazon SES / Resend; gönderici alan adını yeniden doğrulayın (SPF/DKIM/DMARC). |
| SMS (Twilio) | SMS (sms) | Twilio hesap bilgileri aynen; Netgsm de desteklenir. |
Her kanalda Doğrula düğmesi (POST /v1/apps/{app_id}/channels/{channel}/validate) sağlayıcıya test bağlantısı yapar.
3. Player (abonelik) göçü
3.1 npm run migrate:onesignal
Araç OneSignal "View devices" API'sinden (GET /players, 300'lük sayfalar) ya da büyük uygulamalar için POST /players/csv_export ile cihazları çeker, Pushizo'nun onesignal_players CSV biçimine yazar ve istenirse doğrudan içe aktarmayı başlatıp tamamlanana kadar izler.
# Yalnızca CSV üret
npm run migrate:onesignal -- --onesignal-app-id <ONESIGNAL_APP_ID> --onesignal-key <REST_API_KEY> --out players.csv
# Büyük uygulama: OneSignal csv_export (gz) kullan
npm run migrate:onesignal -- --onesignal-app-id <id> --onesignal-key <key> --csv-export --out players.csv
# CSV üret ve Pushizo'ya içe aktar (iş tamamlanana kadar bekler)
npm run migrate:onesignal -- --onesignal-app-id <id> --onesignal-key <key> \
--hs-api-url https://api.pushizo.com --hs-api-key hs_live_… --hs-app-id app_…
```text
Ortam değişkenleri de kabul edilir: `ONESIGNAL_APP_ID`, `ONESIGNAL_REST_API_KEY`, `HS_API_URL`, `HS_API_KEY`, `HS_APP_ID`. Çıktı stdout'a JSON olarak yazılır: `{ players, file, importId, importStatus, stats }`.
CSV başlıkları OneSignal'in dışa aktarma biçimiyle aynıdır (`id, identifier, session_count, language, timezone, game_version, device_os, device_type, device_model, ad_id, tags, last_active, playtime, amount_spent, created_at, invalid_identifier, badge_count, external_user_id, notification_types`). OneSignal panelinden indirdiğiniz "Export" dosyasını da aracı kullanmadan doğrudan yükleyebilirsiniz.
### 3.2 CSV'yi elle içe aktarma
```bash
curl -X POST "https://api.example.com/v1/apps/app_…/imports" \
-H "Authorization: Bearer hs_live_…" \
-F type=onesignal_players -F file=@players.csv
# → { "id": "imp_…", "status": "queued" }
curl "https://api.example.com/v1/apps/app_…/imports/imp_…" -H "Authorization: Bearer hs_live_…"
# → { "status": "completed", "stats": { "processed", "created_users", "updated_users", "created_subscriptions", "skipped", "errors" }, "error_report_url": "…" }
```text
Dashboard eşdeğeri: **Kitle → Dışa / İçe Aktarım**. Dosya sınırı 50 MB ve 500.000 satırdır; daha büyük dosyaları bölün. İçe aktarma idempotenttir: aynı dosya ikinci kez yüklendiğinde kullanıcı ve abonelik yinelenmez.
### 3.3 Alan eşleme tablosu (`mapOneSignalRow`)
| OneSignal sütunu | Pushizo alanı | Kural |
|------------------|------------------|-------|
| `external_user_id` | `external_id` | Boşsa ve `identifier` varsa anonim kullanıcı oluşturulur; ikisi de boşsa satır hata raporuna düşer. |
| `identifier` | abonelik `token` | Push token / e-posta / telefon. E-posta küçük harfe, telefon E.164'e normalize edilir; geçersizse satır atlanır. |
| `device_type` | abonelik `channel` | `0` → `ios_push`; `1`, `2`, `13` → `android_push`; `11` → `email`; `14` → `sms`; `5`, `7`, `8`, `9`, `10` (web) → yalnızca kullanıcı oluşturulur (bkz. aşağı). Diğer değerler: abonelik yok. |
| `invalid_identifier` = `t` | abonelik `status: invalid` | |
| `notification_types` < 0 | abonelik `status: unsubscribed` | Aksi halde `subscribed`. |
| `tags` (JSON) | kullanıcı `tags` | Değerler metne çevrilir (nesne/dizi JSON metni olur, `null` atlanır). Geçersiz JSON → satır hatası. |
| `language` | `properties.language`, `device.locale` | |
| `timezone` (UTC'den saniye) | `device.timezone` | Tam saat ise `Etc/GMT∓h` (UTC+3 → `Etc/GMT-3`), değilse boş. |
| `device_model`, `device_os`, `game_version` | `device.model`, `device.os_version`, `device.app_version` | |
| `session_count`, `created_at`, `last_active` | `session_count`, `first_seen_at`, `last_active_at` | |
| `id`, `ad_id`, `playtime`, `amount_spent`, `badge_count` | — | Aktarılmaz. |
**Web push abonelikleri CSV ile taşınamaz**: OneSignal dışa aktarımı `p256dh`/`auth` anahtarlarını içermez ve VAPID anahtar çifti değiştiği için tarayıcı abonelikleri yeniden oluşturulmalıdır. Web ziyaretçileri Pushizo Web SDK'sı yüklendiğinde (`autoResubscribe: true` varsayılanıyla) izin zaten verilmişse sessizce yeniden abone olur; bu yüzden web için çift yazma dönemini uzun tutun.
### 3.4 Göç sonrası kontrol
- **Abonelikler** sayfasında kanal dağılımını OneSignal "Audience" sayılarıyla karşılaştırın.
- Bir cihazı **Test cihazı** yapıp **Mesajlar → Test push gönder** ile gerçek teslimatı doğrulayın (OneSignal'den gelen APNs/FCM token'ları Pushizo'dan de çalışır).
- `error_report_url` dosyasındaki satırları inceleyin (`line, error, row`).
## 4. API eşleme tablosu
Kimlik doğrulama: OneSignal `Authorization: Basic <REST_API_KEY>` → Pushizo `Authorization: Bearer hs_live_…`. Hata zarfı `{ "error": { "code": "HS_…", "message", "details", "request_id" } }` ([hata kataloğu](/docs/errors)). Sayfalama OneSignal'deki `offset`/`limit` yerine `cursor`/`limit` (`next_cursor`, `has_more`).
### 4.1 Uçlar
| OneSignal REST | Pushizo public API | Fark |
|----------------|----------------------|------|
| `POST /notifications` | `POST /v1/notifications` | Gövdede `app_id`; yanıt `202 { id, status, recipients_estimate }`. Idempotency için `Idempotency-Key` başlığı ya da `idempotency_key` alanı (OneSignal `external_id`). |
| `GET /notifications?app_id=` | `GET /v1/notifications?app_id=` | Cursor sayfalama, `status` filtresi. |
| `GET /notifications/{id}` | `GET /v1/notifications/{message_id}` | `counts` nesnesi (`queued, sent, delivered, failed, opened, clicked, suppressed…`). |
| `DELETE /notifications/{id}` | `DELETE /v1/notifications/{message_id}` | Yalnızca zamanlanmış/kuyruktaki mesaj iptal edilir. |
| `GET /notifications/{id}/history` | `GET /v1/notifications/{message_id}/history` | CSV işi; tamamlanınca `download_url`. |
| `POST /notifications/{id}/export_events` | `POST /v1/notifications/{message_id}/export_events` | CSV işi; `download_url`. |
| `GET /players` (view devices) | `GET /v1/apps/{app_id}/users` + `GET /v1/apps/{app_id}/subscriptions/{id}` | Cihaz kavramı yerine **kullanıcı** ve **abonelik**; aboneliğin sahibi `GET …/subscriptions/{id}/user/identity`. |
| `GET /players/{id}` | `GET /v1/apps/{app_id}/subscriptions/{subscription_id}` | Alanlar: `channel`, `status`, `token_masked`, `device`, `user_id`. |
| `POST /players` / `PUT /players/{id}` | `POST /v1/apps/{app_id}/users` (aboneliklerle) / `PATCH …/subscriptions/{id}` | Sunucu tarafı cihaz kaydı için `users.upsert` + `subscriptions[]`. |
| `PUT /players/{id}` (tag güncelleme) | `PATCH /v1/apps/{app_id}/users/by/{label}/{value}` | Merge-patch `tags`, `properties`, `deltas`. `null` değer tag'i siler. |
| `POST /players/csv_export` | `POST /v1/apps/{app_id}/exports` (`type: users` \| `subscriptions` \| `events` \| `deliveries`) | `GET …/exports/{id}` → `download_url`. |
| `GET /users/by/{alias_label}/{alias_id}` | `GET /v1/apps/{app_id}/users/by/{label}/{value}` | Aynı yol; `hs_id` OneSignal `onesignal_id`'nin karşılığıdır. |
| `POST /users` | `POST /v1/apps/{app_id}/users` | `external_id`, `tags`, `properties`, `language`, `subscriptions[]`. |
| `PATCH /users/by/{label}/{value}` | `PATCH /v1/apps/{app_id}/users/by/{label}/{value}` | |
| `DELETE /users/by/{label}/{value}` | `DELETE /v1/apps/{app_id}/users/by/{label}/{value}` | Abonelikler silinir, olaylar anonimleşir. |
| `GET/PATCH /users/by/{label}/{value}/identity` | `GET/PATCH /v1/apps/{app_id}/users/by/{label}/{value}/identity` | Alias ekleme; `DELETE …/identity/{alias_label}` kaldırır. |
| `POST /users/by/{label}/{value}/subscriptions` | `POST /v1/apps/{app_id}/users/by/{label}/{value}/subscriptions` | `{ channel, token, device? }`. |
| `PATCH /subscriptions/{id}` | `PATCH /v1/apps/{app_id}/subscriptions/{subscription_id}` | `opted_in`, `device`, `test_device`. |
| `DELETE /subscriptions/{id}` | `DELETE /v1/apps/{app_id}/subscriptions/{subscription_id}` | |
| `PATCH /subscriptions_by_token/{token_type}/{token}` | `PATCH /v1/apps/{app_id}/subscriptions_by_token/{token_type}/{token}` | OneSignal tür adları (`iOSPush`, `AndroidPush`, `ChromePush`, `FirefoxPush`, `SafariPush`, `Email`, `SMS`) da kabul edilir. |
| `POST /subscriptions/{id}/owner` | `POST /v1/apps/{app_id}/subscriptions/{subscription_id}/owner` | `{ external_id }`. |
| `GET /subscriptions/{id}/user/identity` | `GET /v1/apps/{app_id}/subscriptions/{subscription_id}/user/identity` | |
| `POST /apps/{app_id}/custom_events` | `POST /v1/apps/{app_id}/events` | `[{ external_id \| subscription_id, name, properties, value, occurred_at }]`, en fazla 500. |
| `GET /apps/{app_id}/outcomes` | `GET /v1/apps/{app_id}/analytics/conversions` | Dönüşüm olayı `event_definitions.is_conversion` ile işaretlenir (`PATCH …/events/definitions/{name}`). |
| `GET/POST /apps/{app_id}/segments`, `DELETE …/segments/{id}` | `GET/POST /v1/apps/{app_id}/segments`, `PATCH/DELETE …/{id}` | Filtre AST (`{ field, op, value }` koşulları, `all`/`any`/`none` grupları; ör. `tags.plan`, `properties.city`, `language`, `event`); `POST …/segments/preview` sayım + örnek kullanıcılar. |
| `GET/POST /templates` | `GET/POST /v1/apps/{app_id}/templates`, `PATCH/DELETE …/{id}`, `POST …/{id}/render` | Liquid; sürüm geçmişi `…/versions`, başka app'e kopya `…/copy_to_app`. |
| `GET /apps`, `GET /apps/{id}` | `GET /v1/apps`, `GET /v1/apps/{app_id}` | `PATCH /v1/apps/{app_id}` ayarları günceller. |
| `POST /apps` | `POST /v1/apps` | Org geneli `admin` anahtarı. |
| Journeys (yalnızca panel) | `GET/POST /v1/apps/{app_id}/journeys`, `…/draft`, `…/publish`, `…/runs` | API ile tasarlanabilir ve tetiklenebilir ([journey'ler](/docs/journeys)). |
| In-App Messages (yalnızca panel) | `GET/POST /v1/apps/{app_id}/in-app-messages` | Blok tabanlı; HTML yok. |
| Live Activities | — | v2 kapsamı (ADR-0017). |
Postman koleksiyonu (`api/postman_collection.json`) tüm bu uçları `{{baseUrl}}`, `{{apiKey}}`, `{{appId}}` değişkenleriyle içerir.
### 4.2 `POST /notifications` gövdesi
Hedefleme alanları Pushizo'da `target` nesnesi altında toplanır; içerik alanlarının çoğu aynı adı taşır.
| OneSignal | Pushizo | Not |
|-----------|-----------|-----|
| `included_segments` / `excluded_segments` | `target.included_segments` / `target.excluded_segments` | Segment kimlikleri (`seg_…`) ya da adları. |
| `include_aliases: { external_id: [] }` | `target.include_aliases` veya `target.include_external_ids` | En fazla 2000. |
| `include_subscription_ids` (eski `include_player_ids`) | `target.include_subscription_ids` | |
| `include_email_tokens` | `target.email_to` | Aboneliği olmayan adres için anonim kullanıcı + abonelik oluşturulur. |
| `include_phone_numbers` | `target.include_phone_numbers` | E.164. |
| `filters` | `target.filters` | Segment AST (OneSignal `field/relation/value` yerine `{ field: "tags.plan", op: "eq", value: "pro" }` ve `all`/`any`/`none` grupları). |
| `target_channel: "push" \| "email" \| "sms"` | `target.channels: ["ios_push", "android_push", "web_push", "email", "sms"]` | Verilmezse içerikten çıkarılır; SMS yalnızca açıkça. |
| `isIos`, `isAndroid`, `isAnyWeb` | `target.channels` | |
| `headings`, `contents`, `subtitle`, `url`, `app_url`, `data`, `buttons`, `big_picture`, `ios_attachments`, `ios_sound`, `ios_category`, `thread_id`, `ios_relevance_score`, `target_content_identifier`, `android_channel_id`, `android_group`, `android_sound`, `small_icon`, `large_icon`, `android_accent_color`, `android_visibility`, `chrome_web_icon`, `chrome_web_image`, `chrome_web_badge`, `web_push_topic`, `web_buttons`, `content_available`, `mutable_content`, `template_id`, `custom_data`, `send_after`, `delayed_option`, `delivery_time_of_day`, `ttl`, `priority`, `collapse_id`, `email_subject`, `email_body`, `email_preheader`, `email_from_name`, `email_from_address`, `email_reply_to_address`, `disable_email_click_tracking`, `sms_from`, `sms_media_urls`, `include_unsubscribed`, `name` | aynı ad | `priority` OneSignal'deki gibi `5` (normal) ya da `10` (yüksek); `ttl` saniye. |
| `ios_badgeType`, `ios_badgeCount` | `ios_badge_type`, `ios_badge_count` | snake_case. |
| `ios_interruption_level` | `ios_interruption_level` | `passive`, `active`, `time-sensitive`, `critical` ([critical alert rehberi](/docs/channels/ios-critical-alerts)). |
| `external_id` (idempotency) | `idempotency_key` ya da `Idempotency-Key` başlığı | Aynı anahtar + aynı gövde aynı yanıtı döndürür; farklı gövde `409 HS_IDEMPOTENCY_CONFLICT`. |
| `send_after` + `delayed_option: "timezone"` | aynı | `delivery_time_of_day: "09:00"`. |
| `throttle_rate_per_minute` | `throttle_rate_per_minute` | |
| `email_mjml` | `email_mjml` | OneSignal'de yok; MJML derlemesi Pushizo'ya özgüdür. |
| — | `add_to_inbox`, `inbox_ttl`, `category`, `labels`, `one_per_user`, `frequency_cap_exempt`, `quiet_hours_exempt`, `require_interaction` | Pushizo ek alanları. |
Örnek (Node sunucu SDK'sı):
```ts
import { Pushizo } from '@pushizo/node';
const hs = new Pushizo({ apiKey: process.env.HS_API_KEY!, appId: process.env.HS_APP_ID, baseUrl: 'https://api.example.com' });
await hs.notifications.create({
idempotency_key: `order-${order.id}`,
target: { include_aliases: { external_id: [userId] } },
headings: { tr: 'Siparişiniz yola çıktı', en: 'Your order has shipped' },
contents: { tr: 'Kargo takip bağlantısı içeride', en: 'Tracking link inside' },
url: `https://example.com/orders/${order.id}`,
data: { order_id: order.id },
});
```text
### 4.3 Webhook'lar
OneSignal "Event Streams" yerine Pushizo **webhook endpoint'leri** (`/v1/apps/{app_id}/webhooks`) HMAC imzalı (`Pushizo-Signature: t=…,v1=…`) olaylar gönderir: `delivery.*`, `subscription.*`, `user.*`, `event.received`, `conversion.created`, `campaign.*`, `journey.*`, `in_app.*`. İmza doğrulama ve yeniden deneme planı için [webhook'lar](/docs/webhooks).
## 5. SDK değişim rehberi
Tüm istemci SDK'larında gizli anahtar yoktur; yalnızca **App ID** (ve kendi API adresiniz) kullanılır. `identity verification` zorunluysa `login` ikinci parametre olarak sunucunuzun ürettiği `identity_hash = HMAC-SHA256(sdk_secret, external_id)` değerini alır (OneSignal'deki JWT yerine).
### 5.1 Web
| OneSignal v16 (Web) | Pushizo Web SDK (`@pushizo/web`) |
|---------------------|-----------------------------------------|
| `<script src="https://cdn.onesignal.com/sdks/web/v16/OneSignalSDK.page.js">` + `OneSignalDeferred.push(async (OneSignal) => { await OneSignal.init({ appId }) })` | `<script src="https://api.example.com/sdk-web/PushizoSDK.js" async>` + `PushizoDeferred.push(async (Pushizo) => { await Pushizo.init({ appId: 'app_…', apiUrl: 'https://api.example.com' }) })` |
| `OneSignalSDKWorker.js` site kökünde | `PushizoSDKWorker.js` site kökünde (`importScripts(apiUrl + '/sdk-web/PushizoSDKWorker.js')`); `serviceWorkerPath` / `serviceWorkerScope` seçenekleri aynı anlamda |
| `OneSignal.login(externalId, jwtToken?)` | `Pushizo.login(externalId, identityHash?)` |
| `OneSignal.logout()` | `Pushizo.logout()` |
| `OneSignal.User.addTag / addTags / removeTag / removeTags / getTags` | aynı adlar: `Pushizo.User.addTag / addTags / removeTag / removeTags / getTags` |
| `OneSignal.User.addAlias / addAliases / removeAlias` | aynı adlar |
| `OneSignal.User.addEmail / removeEmail / addSms / removeSms` | aynı adlar (adres istemcide normalize edilir) |
| `OneSignal.User.setLanguage` | `Pushizo.User.setLanguage` |
| `OneSignal.User.PushSubscription.optIn / optOut / id / token / optedIn` + `addEventListener('change')` | `Pushizo.User.pushSubscription.optIn / optOut / id / optedIn / state` + `addObserver(cb)` (geri dönen fonksiyon dinleyiciyi kaldırır) |
| `OneSignal.Notifications.requestPermission()` / `permission` | `Pushizo.Notifications.requestPermission()` / `permission` (`'default' \| 'granted' \| 'denied' \| 'unsupported'`) |
| `OneSignal.Notifications.addEventListener('click', cb)` | `Pushizo.Notifications.addClickListener(({ notification, result }) => …)` |
| `OneSignal.Notifications.addEventListener('foregroundWillDisplay', cb)` | `Pushizo.Notifications.addForegroundLifecycleListener((e) => e.preventDefault())` |
| `OneSignal.Slidedown.promptPush / promptEmail / promptSms / promptPushCategories` | `Pushizo.Slidedown.promptPush / promptEmail / promptSms / promptEmailAndSms` (kategori tercihleri için `User.setPreferences`) |
| `promptOptions.slidedown`, `welcomeNotification`, `notificationClickHandlerMatch/Action`, `notifyButton` | `slidedown`, `welcomeNotification`, `notificationClickHandlerMatch` / `notificationClickHandlerAction`, `bell` (init seçenekleri) |
| `OneSignal.Session.sendOutcome / sendUniqueOutcome / sendOutcomeWithValue` | `Pushizo.Session.sendEvent(name, properties?, value?)` |
| `OneSignal.User.PushSubscription` bildirim kutusu yok | `Pushizo.Inbox.list / markRead / markAllRead / archive / unreadCount / addObserver` |
| In-app (yalnızca mobil) | `Pushizo.InAppMessages.addTrigger / addTriggers / removeTrigger / clearTriggers / setScreen / paused / addClickListener / addLifecycleListener` (web dahil) |
| `OneSignal.setConsentRequired(true)` + `setConsentGiven(true)` | `init({ requiresPrivacyConsent: true })` + `Pushizo.setConsentGiven(true)` |
| `OneSignal.Debug.setLogLevel` | `Pushizo.Debug.setLogLevel('debug')` |
Ayrıntı: [Web SDK](/docs/sdk/web), çatı entegrasyonları: [React](/docs/integrations/react), [Vue](/docs/integrations/vue), [Angular](/docs/integrations/angular), iOS Safari PWA: [iOS web push](/docs/channels/ios-pwa-web-push).
### 5.2 iOS
| OneSignal iOS SDK 5 | Pushizo iOS SDK (`Pushizo`, `PushizoExtension`) |
|---------------------|----------------------------------------------------------|
| `OneSignal.initialize("APP_ID", withLaunchOptions:)` | `Pushizo.initialize("app_…", launchOptions:) { options in options.apiUrl = "https://api.example.com" }` |
| Swizzling ile token | Swizzling yok: `didRegisterForRemoteNotificationsWithDeviceToken` içinde `Pushizo.setDeviceToken(deviceToken)`, hata durumunda `Pushizo.setDeviceTokenError(error)` |
| `OneSignal.login("id")` / `logout()` | `Pushizo.login("id", identityHash: nil)` / `Pushizo.logout()` |
| `OneSignal.User.addTag/addTags/removeTag/getTags/setLanguage` | aynı adlar |
| `OneSignal.User.addEmail/addSms/removeEmail/removeSms` | aynı adlar |
| `OneSignal.User.pushSubscription.optIn()/optOut()/id/token/optedIn` + `addObserver` | `Pushizo.User.pushSubscription.optIn()/optOut()/id/token`, `await state()`, `addObserver { state in }` |
| `OneSignal.Notifications.requestPermission(_:fallbackToSettings:)` | `Pushizo.Notifications.requestPermission { granted in }` (`await permission()` ile okuma) |
| `OneSignal.Notifications.addPermissionObserver` | `addPermissionObserver { granted in }` → `HSListenerToken`; `removePermissionObserver(token)` |
| `addClickListener` / `addForegroundLifecycleListener` | aynı adlar; belirteç döndürür (`removeClickListener(token)`) |
| `OneSignalNotificationServiceExtension` | `PushizoNotificationServiceExtension(appId:apiUrl:appGroup:)` → `didReceive`, `serviceExtensionTimeWillExpire` |
| `OneSignal.InAppMessages.addTrigger/removeTrigger/clearTriggers/paused` | aynı adlar + `setScreen`, `addClickListener`, `addLifecycleListener` |
| `OneSignal.Session.sendOutcome(_:)` | `Pushizo.Session.sendEvent("name", properties:, value:)` |
| `OneSignal.setConsentRequired/setConsentGiven` | `options.requiresPrivacyConsent = true` + `Pushizo.setConsentGiven(true)` |
| `OneSignal.Notifications.clearAll()` | `Pushizo.Notifications.clearAll()`, `removeGroupedNotifications`, `removeNotification(identifier:)` |
| Live Activities | v2 |
Aynı APNs `.p8` anahtarı kullanıldığından mevcut cihaz token'ları Pushizo'dan de teslim alır; SDK ilk açılışta token'ı `POST /subscriptions` ile kaydeder (göçte aktarılmış abonelikle `(app, channel, token)` üzerinden birleşir). Ayrıntı: [iOS SDK](/docs/sdk/ios).
### 5.3 Android
| OneSignal Android SDK 5 | Pushizo Android SDK (`com.pushizo:pushizo-android`) |
|-------------------------|---------------------------------------------------------------|
| `OneSignal.initWithContext(this, "APP_ID")` | `Pushizo.initialize(this, "app_…", Pushizo.Options(apiUrl = "https://api.example.com"))` (Application `onCreate`) |
| `OneSignal.login("id")` / `logout()` | `Pushizo.login("id", identityHash = null)` / `Pushizo.logout()` |
| `OneSignal.User.addTag/addTags/removeTag/getTags/setLanguage` | aynı adlar |
| `OneSignal.User.addEmail/addSms/removeEmail/removeSms` | aynı adlar (`emails`, `smsNumbers` listeleri) |
| `OneSignal.User.pushSubscription.optIn()/optOut()/id/token/optedIn` + `addObserver` | aynı adlar (`state` ek) |
| `OneSignal.Notifications.requestPermission(fallbackToSettings)` (coroutine) | `Pushizo.Notifications.requestPermission(activity)` + `onRequestPermissionsResult` içinde `Pushizo.Notifications.onPermissionResult()` (`PERMISSION_REQUEST_CODE`) |
| `addPermissionObserver` | `addPermissionObserver(observer)` / `removePermissionObserver(observer)` |
| `addClickListener` / `addForegroundLifecycleListener` | aynı adlar |
| `OneSignal.InAppMessages.addTrigger/…/paused` | aynı adlar + `setScreen`, `addClickListener`, `addLifecycleListener` |
| `OneSignal.Session.sendOutcome` | `Pushizo.Session.sendEvent("name", mapOf(...), value)` |
| `OneSignal.consentRequired = true` / `consentGiven = true` | `Options(requiresPrivacyConsent = true)` + `Pushizo.setConsentGiven(true)` |
| `OneSignal.Notifications.clearAllNotifications()` | `Pushizo.Notifications.clearAll()`, `removeNotification`, `removeGroupedNotifications` |
| Bildirim kanalları (panelden) | Dashboard → Ayarlar → Android bildirim kategorileri (`android_channels`); `android_channel_id` ile seçilir |
Aynı Firebase projesi kullanıldığı sürece FCM token'ları değişmez. Ayrıntı: [Android SDK](/docs/sdk/android).
### 5.4 Flutter
| `onesignal_flutter` | `pushizo_flutter` |
|---------------------|----------------------|
| `OneSignal.initialize("APP_ID")` | `await Pushizo.initialize('app_…', apiUrl: 'https://api.example.com')` |
| `OneSignal.login/logout` | `Pushizo.login(externalId, identityHash: null)` / `Pushizo.logout()` |
| `OneSignal.User.addTags/removeTag/getTags/setLanguage` | aynı adlar (hepsi `Future`) |
| `OneSignal.User.addEmail/addSms/removeEmail/removeSms` | aynı adlar |
| `OneSignal.User.pushSubscription.optIn/optOut/id/token/optedIn` + `addObserver` | `Pushizo.User.pushSubscription.optIn()/optOut()`, `await state`, `changes.listen(...)` |
| `OneSignal.Notifications.requestPermission(true)` | `await Pushizo.Notifications.requestPermission()` |
| `OneSignal.Notifications.addPermissionObserver` | `Pushizo.Notifications.addPermissionObserver(fn)` ya da `permissionStream.listen(...)` |
| `addClickListener` / `addForegroundWillDisplayListener` | `Pushizo.Notifications.clickStream.listen(...)` / `foregroundStream.listen(...)` |
| `OneSignal.InAppMessages.addTrigger/…/paused(bool)` | aynı adlar + `setScreen`, `setPaused(bool)`, `clickStream`, `lifecycleStream`, `setNavigatorKey` |
| `OneSignal.Session.sendOutcome` | `Pushizo.Session.sendEvent(name, properties:, value:)` |
| `OneSignal.consentRequired(true)` / `consentGiven(true)` | `initialize(..., requiresPrivacyConsent: true)` + `Pushizo.setConsentGiven(true)` |
| — | `Pushizo.Inbox.list/markRead/markAllRead/archive/unreadCount`, `Pushizo.User.getPreferences/setPreferences` |
Ayrıntı: [Flutter SDK](/docs/sdk/flutter).
### 5.5 Köprü SDK'ları için ortak notlar
Aşağıdaki tablolarda OneSignal sütunu ilgili paketin **v5** API adlarını esas alır; daha eski (v3/v4) bir OneSignal sürümünden geçiyorsanız önce çağrıları v5 karşılıklarına eşleyin. Tüm köprü SDK'larında:
- Paket değişimi tek seferdedir: OneSignal paketini (ve Expo için `onesignal-expo-plugin`) kaldırıp Pushizo paketini ekleyin, yerel projeyi yeniden üretin (`pod install`, `npx cap sync`, `cordova prepare`, `expo prebuild`). Pushizo iOS'ta `UNUserNotificationCenter` delegesini üstlendiğinden aynı sürümde iki push SDK'sı birlikte bulunmamalıdır.
- `initialize` App ID'nin yanında kendi API adresinizi (`apiUrl`) alır; REST API anahtarı istemciye gömülmez.
- OneSignal "outcome" çağrıları (`addOutcome`, `addUniqueOutcome`, `addOutcomeWithValue` ve C# karşılıkları) tek bir `Session.sendEvent(name, properties?, value?)` çağrısına dönüşür; aynı ad in-app `event` tetiklerini de çalıştırır. Tekillik (unique) gerekiyorsa uygulamada ya da journey/segment koşulunda sağlayın.
- Bildirim kutusu (`Inbox`) ve kategori tercihleri (`User.getPreferences` / `setPreferences`) OneSignal'de karşılığı olmayan eklemelerdir (bkz. 5.11).
### 5.6 React Native
| `react-native-onesignal` 5 | `@pushizo/react-native` |
|----------------------------|----------------------------|
| Expo: `["onesignal-expo-plugin", { "mode": "development" }]` | `["@pushizo/react-native", { "mode": "development", "appId": "app_…" }]` (plugin pakete dahildir; NSE hedefi `PushizoNotificationServiceExtension`) |
| Bare iOS: token swizzling ile alınır | Swizzling yok: `AppDelegate` içinde `Pushizo.setDeviceToken(deviceToken)` / `Pushizo.setDeviceTokenError(error)` (Expo plugin otomatik ekler) |
| `OneSignal.initialize('APP_ID')` | `await Pushizo.initialize('app_…', { apiUrl: 'https://api.example.com' })` |
| `OneSignal.login(externalId)` / `OneSignal.logout()` | `Pushizo.login(externalId, identityHash?)` / `Pushizo.logout()` |
| `OneSignal.User.addTag / addTags / removeTag / removeTags / getTags` | aynı adlar |
| `OneSignal.User.addAlias / addAliases / removeAlias` | aynı adlar |
| `OneSignal.User.addEmail / removeEmail / addSms / removeSms`, `setLanguage` | aynı adlar |
| `OneSignal.User.getExternalId()` / `getOnesignalId()` | `Pushizo.User.getExternalId()` / `getPushizoId()` |
| `OneSignal.User.pushSubscription.getIdAsync / getTokenAsync / getOptedInAsync / optIn / optOut` + `addEventListener('change', …)` | aynı adlar (`{ previous, current }`) |
| `OneSignal.Notifications.requestPermission(fallbackToSettings)` | `Pushizo.Notifications.requestPermission()` |
| `OneSignal.Notifications.getPermissionAsync()` | `Pushizo.Notifications.getPermissionAsync()` (eşzamanlı son değer: `permission`) |
| `OneSignal.Notifications.addEventListener('click' \| 'foregroundWillDisplay' \| 'permissionChange', …)` | aynı olay adları; `e.preventDefault()` ve `e.getNotification().display()` aynı |
| `OneSignal.Notifications.clearAll()` | `Pushizo.Notifications.clearAll()` |
| `OneSignal.InAppMessages.addTrigger / addTriggers / removeTrigger / removeTriggers / clearTriggers / setPaused / getPaused` | aynı adlar + `getTriggers`, `setScreen` |
| `OneSignal.InAppMessages.addEventListener('click' \| 'willDisplay' \| 'didDisplay' \| 'willDismiss' \| 'didDismiss', …)` | aynı olay adları; olay gövdesi `{ messageId, actionId, url }` / `{ messageId }` |
| `OneSignal.Session.addOutcome / addUniqueOutcome / addOutcomeWithValue` | `Pushizo.Session.sendEvent(name, properties?, value?)` |
| `OneSignal.setConsentRequired(true)` + `setConsentGiven(true)` | `initialize(…, { requiresPrivacyConsent: true })` + `Pushizo.setConsentGiven(true)` |
| `OneSignal.Debug.setLogLevel(LogLevel.Verbose)` | `Pushizo.Debug.setLogLevel(LogLevel.Debug)` |
| — | `Pushizo.Inbox.*`, `Pushizo.User.getPreferences / setPreferences` |
Ayrıntı: [React Native SDK](/docs/sdk/react-native).
### 5.7 Capacitor (Ionic)
OneSignal Ionic/Capacitor uygulamalarında `onesignal-cordova-plugin` paketini kullanır; Pushizo'da ayrı bir Capacitor eklentisi vardır (web'de `@pushizo/web`'e devreder).
| `onesignal-cordova-plugin` 5 (Capacitor) | `@pushizo/capacitor` |
|------------------------------------------|-------------------------|
| `npm install onesignal-cordova-plugin` | `npm install @pushizo/capacitor` (+ web için `@pushizo/web`), `npx cap sync` |
| iOS token swizzling ile alınır | `AppDelegate.swift` içinde `capacitorDidRegisterForRemoteNotifications` / `capacitorDidFailToRegisterForRemoteNotifications` bildirimlerini gönderin |
| `OneSignal.initialize('APP_ID')` | `await Pushizo.initialize('app_…', { apiUrl: 'https://api.example.com' })` |
| `OneSignal.login / logout` | `Pushizo.login(externalId, identityHash?)` / `Pushizo.logout()` (Promise) |
| `OneSignal.User.addTag / addTags / removeTag / removeTags / getTags`, `addAlias / addAliases / removeAlias`, `addEmail / removeEmail / addSms / removeSms`, `setLanguage` | aynı adlar (hepsi Promise) |
| `OneSignal.User.pushSubscription.getIdAsync / getTokenAsync / getOptedInAsync / optIn / optOut` | `Pushizo.User.pushSubscription.getId / getToken / getOptedIn / getState / optIn / optOut` |
| `OneSignal.User.pushSubscription.addEventListener('change', cb)` | `Pushizo.User.pushSubscription.addObserver((state) => …)` (dönen fonksiyon aboneliği kaldırır) |
| `OneSignal.Notifications.requestPermission(true)` | `await Pushizo.Notifications.requestPermission()` |
| `OneSignal.Notifications.addEventListener('click', cb)` | `Pushizo.Notifications.addClickListener(({ notification, result }) => …)` |
| `OneSignal.Notifications.addEventListener('foregroundWillDisplay', cb)` | `Pushizo.Notifications.addForegroundLifecycleListener((e) => e.preventDefault())`; erteleyip göstermek için `await e.display()` |
| `OneSignal.Notifications.addEventListener('permissionChange', cb)` | `Pushizo.Notifications.addPermissionObserver(({ permission }) => …)` |
| `OneSignal.Notifications.clearAll()` | `Pushizo.Notifications.clearAll()`, `removeGroupedNotifications`, `removeNotification` (yalnızca Android) |
| `OneSignal.InAppMessages.addTrigger / addTriggers / removeTrigger / removeTriggers / clearTriggers / setPaused / getPaused` | aynı adlar + `getTriggers`, `setScreen` |
| `OneSignal.InAppMessages.addEventListener('click' \| 'willDisplay' \| …, cb)` | `Pushizo.InAppMessages.addClickListener(cb)` / `addLifecycleListener({ willDisplay, didDisplay, willDismiss, didDismiss })` |
| `OneSignal.Session.addOutcome / addUniqueOutcome / addOutcomeWithValue` | `Pushizo.Session.sendEvent(name, properties?, value?)` |
| `OneSignal.setConsentRequired(true)` + `setConsentGiven(true)` | `initialize(…, { requiresPrivacyConsent: true })` + `await Pushizo.setConsentGiven(true)` |
| `OneSignal.Debug.setLogLevel(…)` | `await Pushizo.Debug.setLogLevel('debug')` |
Ayrıntı: [Capacitor SDK](/docs/sdk/capacitor).
### 5.8 Cordova
| `onesignal-cordova-plugin` 5 | `cordova-plugin-pushizo` |
|------------------------------|-----------------------------|
| `cordova plugin add onesignal-cordova-plugin`, `window.plugins.OneSignal` | `cordova plugin add cordova-plugin-pushizo`, `window.Pushizo` (ve `window.plugins.Pushizo`) |
| Android Gradle ayarı gerekmez | `config.xml` → `GradlePluginKotlinEnabled` ve `GradlePluginGoogleServicesEnabled` (köprü Kotlin'dir) |
| `OneSignal.initialize('APP_ID')` (`deviceready` içinde) | `await Pushizo.initialize('app_…', { apiUrl: 'https://api.example.com' })` |
| Diğer tüm çağrılar | Capacitor tablosuyla (5.7) birebir aynı; tüm metotlar Promise döndürür, hatalar `PushizoError` (`code`) |
iOS'ta APNs token geri çağrıları eklenti tarafından AppDelegate'e çalışma zamanında eklenir; ek kod gerekmez. Ayrıntı: [Cordova SDK](/docs/sdk/cordova).
### 5.9 .NET MAUI
| `OneSignalSDK.DotNet` 5 | `Pushizo.Maui` |
|-------------------------|-------------------|
| `dotnet add package OneSignalSDK.DotNet`, `using OneSignalSDK.DotNet;` | `dotnet add package Pushizo.Maui`, `using PushizoSdk.Maui;` |
| `OneSignal.Initialize("APP_ID")` | `builder.UsePushizo("app_…", o => o.ApiUrl = "https://api.example.com")` (`MauiProgram.cs`) ya da `Pushizo.Initialize("app_…", options)` |
| iOS token swizzling ile alınır | `AppDelegate` içinde `PushizoIos.RegisteredForRemoteNotifications(deviceToken)` / `FailedToRegisterForRemoteNotifications(error)` |
| `OneSignal.Login("id")` / `OneSignal.Logout()` | `Pushizo.Login("id", identityHash)` / `Pushizo.Logout()` |
| `OneSignal.User.AddTag / AddTags / RemoveTag / RemoveTags`, `AddAlias / AddAliases / RemoveAlias`, `AddEmail / RemoveEmail / AddSms / RemoveSms` | aynı adlar + `GetTags()`, `SetLanguage("tr")` |
| `OneSignal.User.PushSubscription.OptIn() / OptOut() / Id / Token / OptedIn` + `Changed` | aynı adlar (`e.Previous`, `e.Current`) |
| `await OneSignal.Notifications.RequestPermissionAsync(true)` | `await Pushizo.Notifications.RequestPermissionAsync(fallbackToSettings: true)` |
| `OneSignal.Notifications.Clicked / ForegroundWillDisplay / PermissionChanged` | aynı olay adları (`EventHandler`); tıklamada `e.Notification`, `e.ActionId`, `e.Url`; ön planda `e.PreventDefault()` |
| `OneSignal.Notifications.ClearAllNotifications()` | `Pushizo.Notifications.ClearAll()` |
| `OneSignal.InAppMessages.AddTrigger / AddTriggers / RemoveTrigger / RemoveTriggers / ClearTriggers / Paused` + `Clicked / WillDisplay / DidDisplay / WillDismiss / DidDismiss` | aynı adlar + `GetTriggers()`, `SetScreen(name)` |
| `OneSignal.Session.AddOutcome / AddUniqueOutcome / AddOutcomeWithValue` | `Pushizo.Session.SendEvent(name, properties, value)` |
| `OneSignal.ConsentRequired = true` + `OneSignal.ConsentGiven = true` | `o.RequiresPrivacyConsent = true` + `Pushizo.SetConsentGiven(true)` |
| `OneSignal.Debug.LogLevel = …` | `Pushizo.Debug.SetLogLevel(LogLevel.Debug)` |
| — | `Pushizo.Inbox.ListAsync / MarkReadAsync / MarkAllReadAsync / ArchiveAsync / UnreadCountAsync`, `Pushizo.User.GetPreferencesAsync / SetPreferencesAsync` |
Ayrıntı: [.NET MAUI SDK](/docs/sdk/maui).
### 5.10 Unity
| OneSignal Unity SDK 5 | `com.pushizo.unity` |
|-----------------------|------------------------|
| OneSignal UPM paketleri, `using OneSignalSDK;` | Git URL ile UPM paketi (`https://github.com/exec0de/pushizo.git?path=sdk/unity#sdk-v0.1.0`), `using PushizoSdk;`; Project Settings → **Pushizo** (App ID, App Group, `google-services.json` yolu) |
| `OneSignal.Initialize("APP_ID")` | `Pushizo.Initialize("app_…", new PushizoOptions { ApiUrl = "https://api.example.com" })` |
| `OneSignal.Login("id")` / `OneSignal.Logout()` | `Pushizo.Login("id", identityHash)` / `Pushizo.Logout()` |
| `OneSignal.User.AddTag / AddTags / RemoveTag / RemoveTags`, `AddAlias / AddAliases / RemoveAlias`, `AddEmail / RemoveEmail / AddSms / RemoveSms` | aynı adlar + `GetTags()`, `SetLanguage("tr")` |
| `OneSignal.User.PushSubscription.OptIn() / OptOut() / Id / Token / OptedIn` + `Changed` | aynı adlar |
| `await OneSignal.Notifications.RequestPermissionAsync(true)` | `await Pushizo.Notifications.RequestPermissionAsync(fallbackToSettings: true)` |
| `OneSignal.Notifications.Clicked / ForegroundWillDisplay / PermissionChanged += (sender, e) => …` | aynı olay adları, `Action<T>` imzası: `Clicked += e => …`, `PermissionChanged += granted => …` (olaylar ana iş parçacığında gelir) |
| `OneSignal.Notifications.ClearAllNotifications()` | `Pushizo.Notifications.ClearAll()` |
| `OneSignal.InAppMessages.AddTrigger / AddTriggers / RemoveTrigger / RemoveTriggers / ClearTriggers / Paused` + `Clicked / WillDisplay / DidDisplay / WillDismiss / DidDismiss` | aynı adlar + `GetTriggers()`, `SetScreen(name)` |
| `OneSignal.Session.AddOutcome / AddUniqueOutcome / AddOutcomeWithValue` | `Pushizo.Session.SendEvent(name, properties, value)` |
| `OneSignal.ConsentRequired = true` + `OneSignal.ConsentGiven = true` | `new PushizoOptions { RequiresPrivacyConsent = true }` + `Pushizo.SetConsentGiven(true)` |
| `OneSignal.Debug.LogLevel = …` | `Pushizo.Debug.SetLogLevel(LogLevel.Debug)` |
| — | `Pushizo.Inbox.*Async`, `Pushizo.User.GetPreferencesAsync / SetPreferencesAsync` |
Android bağımlılıkları EDM4U ile çözülür ve `Assets/google-services.json` derlemede işlenir (Firebase Unity SDK'sı gerekmez); iOS'ta Push yeteneği, NSE hedefi ve Podfile derleme sonrası işlemle eklenir. Ayrıntı: [Unity SDK](/docs/sdk/unity).
### 5.11 Bildirim kutusu ve tercihler (OneSignal'de olmayanlar)
- **Bildirim kutusu:** `add_to_inbox: true` ile gönderilen mesajlar kullanıcıya özel kutuda listelenir (`Inbox.list`, `markRead`, `markAllRead`, `archive`, `unreadCount`); OneSignal'de karşılığı olmadığından taşınacak veri yoktur.
- **Kategori tercihleri:** OneSignal "push categories" slidedown'ı yerine dashboard'da **Kategoriler** tanımlanır; SDK'lar `User.getPreferences()` / `setPreferences([...])` ile kullanıcı × kanal opt-in yazar. OneSignal'de tag ile yönettiğiniz tercihleri (`tags.promo_optin = "0"`) göç sonrasında bir kerelik `PUT /v1/apps/{app_id}/users/by/external_id/{id}/preferences` çağrısıyla gerçek tercihe çevirebilirsiniz.
## 6. Kesinti planı (çift yazma dönemi)
1. **Hafta 0 — hazırlık:** kanallar doğrulanmış, player göçü tamamlanmış, sunucu entegrasyonu Pushizo'ya de yazıyor (çift yazma: her bildirim/olay OneSignal ve Pushizo'ya birlikte gider; Pushizo'da `test_devices_only: true` ile yalnızca test cihazlarına gönderim yapın ya da hedefi küçük bir segmentle sınırlayın).
2. **Hafta 1–2 — istemci yayını:** Pushizo SDK'sını içeren mobil sürümü yayınlayın, web'de SDK'yı canlıya alın. Her iki SDK aynı uygulamada çalışabilir; ancak **push izni istemini yalnızca Pushizo'dan** yapın (iki SDK birden izin isterse kullanıcı kafa karışıklığı yaşar) ve Android'de yalnızca birinin ön plan bildirimi göstermesine izin verin (`addForegroundLifecycleListener` + `preventDefault()`).
3. **Hafta 2–3 — trafik kaydırma:** işlemsel bildirimleri (sipariş, OTP) Pushizo'dan, kampanyaları dashboard'dan göndermeye başlayın. OneSignal'den gönderimi segment bazında azaltın; **Analitik → Teslimat raporu** ve **Sağlayıcı sağlığı** sayfalarından teslim/açılma oranlarını karşılaştırın.
4. **Hafta 3–4 — kesinti:** OneSignal gönderimlerini durdurun, `migrate:onesignal` ile son bir fark çekimi yapın (içe aktarma idempotenttir; yeni player'lar eklenir, mevcutlar güncellenir), OneSignal webhook/Event Stream'lerini kapatın, REST API anahtarını iptal edin.
5. **Eski uygulama sürümleri:** OneSignal SDK'sı içeren eski sürümler hâlâ OneSignal'e token kaydeder; bu kullanıcılar için haftalık fark çekimini sürümün kullanım payı %1'in altına inene kadar sürdürün.
6. **Geri alma:** çift yazma döneminde OneSignal yapılandırmasını silmeyin; kritik bir sorun olursa gönderimi OneSignal'e geri alabilirsiniz. Kesintiden sonra `.p8`/servis hesabı anahtarlarını OneSignal'den kaldırın.
## 7. Sık karşılaşılan farklar
| Konu | OneSignal | Pushizo |
|------|-----------|------------|
| Kimlik | `onesignal_id` + `external_id` | `hs_id` + `external_id` + özel alias'lar (`users/by/{label}/{value}`) |
| Cihaz birleştirme | `login` cihazı kullanıcıya bağlar | Aynı; anonim kullanıcı birleştirilir, `user.merged` webhook'u üretilir |
| Hata biçimi | `{ "errors": [...] }` | `{ "error": { "code": "HS_…", … } }`, `x-request-id` |
| Hız sınırı | sabit | Plan bazlı (`api_rps`), `X-RateLimit-*` başlıkları, `429 HS_RATE_LIMITED` + `Retry-After` |
| Sessiz saatler / frekans sınırı | panel | App ayarları; mesaj başına `quiet_hours_exempt`, `frequency_cap_exempt` |
| Şablon dili | `{{ tag }}` basit değişkenler | Liquid (`{{ user.tags.name }}`, filtreler, koşullar) |
| Web push kimliği | OneSignal VAPID | Kendi VAPID çiftiniz (kanal kaydında üretilir); abonelikler yeniden oluşur |
| Veri yerleşimi | OneSignal bulutu | Kendi altyapınız (self-hosted) |Runbook: Gerçek cihaza APNs ve FCM test gönderimi
Bu runbook, kanal kimlik bilgilerinin gerçek sağlayıcılarla çalıştığını doğrulamak için izlenir. Faz 3'te "doğrula" ucu sağlayıcıya test bağlantısı kurar; gerçek cihaza gönderim Faz 4 (POST /v1/notifications) ve Faz 5…
Firebase Cloud Messaging'den (FCM) Pushizo'ya göç
Bildirimleri doğrudan Firebase Cloud Messaging (FCM HTTP v1 API, Firebase Admin SDK ya da Firebase konsolu) ile gönderiyorsanız bu rehber Pushizo'ya geçişi anlatır. Pushizo Android'de FCM'i taşıma katmanı olarak…