# راهنمای جامع پیاده‌سازی Pushfa iOS Push

> مرجع Native iOS برای توسعه‌دهنده و Agent — Pushfa iOS SDK v1.0.0 (Beta)

این سند درباره اپ نیتیو iOS است. Web Push روی iPhone/PWA مسیر جداگانه‌ای دارد و در سند Web Push توضیح داده شده است. APIهای هدف‌گیری و ارسال سروری میان همه پلتفرم‌ها مشترک‌اند.

## امنیت و سازگاری

- نسخه فعلی: iOS 15+، Xcode 16.3+، Swift tools 6.1 و Firebase Apple SDK 12.17+.
- فقط `api_public_key` داخل اپ قرار می‌گیرد.
- `api_private_key` و Firebase Service Account فقط در Backend یا پنل Pushfa هستند.
- Bundle ID اپ، Firebase Apple App، APNs Key، `GoogleService-Info.plist` و Service Account پوشفا باید به یک پروژه/اپ مرتبط باشند.
- قابلیت iOS ممکن است با Feature Flag مدیر (`show_ios_sdk`) پنهان شود و در نسخه فعلی Beta است.

## ۱. Firebase، APNs و پنل Pushfa

1. Apple App را در Firebase با Bundle ID دقیق Target اصلی بسازید.
2. `GoogleService-Info.plist` را به Target اپ اضافه کنید.
3. در Xcode، Capabilityهای **Push Notifications** و **Background Modes > Remote notifications** را فعال کنید.
4. در Apple Developer یک APNs Authentication Key معتبر داشته باشید.
5. Key ID، Team ID و فایل `.p8` را در Firebase Console > Cloud Messaging بارگذاری کنید.
6. Service Account همان Firebase Project را فقط در پنل Pushfa ثبت کنید.
7. سرویس نوع iOS بسازید و Public Key را بردارید.

روی دستگاه واقعی تست کنید؛ شبیه‌ساز همه رفتارهای APNs، Background و Extension را قابل‌اعتماد بازتولید نمی‌کند.

## ۲. نصب با Swift Package Manager

در Xcode از **File > Add Package Dependencies** این URL را وارد کنید:

```text
https://github.com/pushfa/pushfa-ios-sdk.git
```

Dependency Rule را `Up to Next Major Version` با حداقل `1.0.0` قرار دهید:

- Product اصلی `Pushfa` فقط به Target اپ.
- Product مستقل `PushfaExtension` فقط به Notification Service Extension.

برای Production به Tag نسخه وابسته شوید، نه branch اصلی. در نصب آفلاین، ZIP رسمی را باز و پوشه دارای `Package.swift` را به‌عنوان Local Package اضافه کنید.

## ۳. Initialize و Permission

```swift
import Pushfa

Task { @MainActor in
    do {
        try await Pushfa.initialize(
            PushfaConfig(
                apiPublicKey: "YOUR_PUBLIC_KEY",
                deepLinkBaseURL: URL(string: "myapp://")
            )
        )

        // این درخواست را بهتر است بعد از Soft Prompt و اقدام کاربر اجرا کنید.
        let granted = try await Pushfa.requestNotificationPermission()
        print("Push permission:", granted)
    } catch {
        print("Pushfa init error:", error)
    }
}
```

دیالوگ iOS پس از ردشدن معمولاً دوباره توسط اپ نمایش داده نمی‌شود. قبل از آن یک صفحه/Sheet کوتاه با دلیل واضح نشان دهید. بعد از رد، فقط با اقدام صریح کاربر لینک Settings را پیشنهاد کنید.

```swift
if let url = URL(string: UIApplication.openSettingsURLString) {
    await UIApplication.shared.open(url)
}
```

## ۴. AppDelegate و APNs Token

اگر Firebase App Delegate Swizzling فعال است، بخش زیادی خودکار انجام می‌شود. اگر `FirebaseAppDelegateProxyEnabled` را خاموش کرده‌اید، Token APNs را دستی به Firebase/Pushfa تحویل دهید. الگوی دقیق امضای متدها را با README نسخه نصب‌شده تطبیق دهید.

```swift
import UIKit
import UserNotifications
import FirebaseCore
import FirebaseMessaging
import Pushfa

final class AppDelegate: NSObject, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
    ) -> Bool {
        FirebaseApp.configure()
        UNUserNotificationCenter.current().delegate = self
        return true
    }

    func application(
        _ application: UIApplication,
        didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
    ) {
        Messaging.messaging().apnsToken = deviceToken
        // در حالت swizzling خاموش، متد handle/register نسخه SDK نصب‌شده را فراخوانی کنید.
    }
}

extension AppDelegate: UNUserNotificationCenterDelegate {
    func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        willPresent notification: UNNotification,
        withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
    ) {
        completionHandler([.banner, .sound, .badge])
    }
}
```

در SwiftUI:

```swift
@main
struct MyApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        WindowGroup { ContentView() }
    }
}
```

اگر اپ از قبل Delegate دارد، آن را جایگزین نکنید؛ Pushfa را در همان چرخه موجود ادغام کنید.

## ۵. Notification Service Extension

برای Rich Image و ثبت دقیق‌تر Delivery در Background یک Target از نوع Notification Service Extension بسازید و فقط Product `PushfaExtension` را به آن متصل کنید:

```swift
import PushfaExtension
import UserNotifications

final class NotificationService: UNNotificationServiceExtension {
    private var pushfaTask: PushfaNotificationServiceTask?

    override func didReceive(
        _ request: UNNotificationRequest,
        withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
    ) {
        pushfaTask = PushfaNotificationServiceExtension.process(
            request,
            contentHandler: contentHandler
        )
    }

    override func serviceExtensionTimeWillExpire() {
        pushfaTask?.serviceExtensionTimeWillExpire()
        pushfaTask = nil
    }
}
```

Deployment Target و App Group/Keychain sharing موردنیاز نسخه SDK را با README همان Tag هماهنگ کنید. Backend پوشفا برای iOS `mutable-content`، `content-available`، `apns-push-type`، expiration و collapse ID را تولید می‌کند.

## ۶. Subscriber، Topic و Alias

```swift
let subscriberId = Pushfa.subscriberId
let pushToken = Pushfa.pushToken

try await Pushfa.subscribeTopic("TOPIC_UUID")
try await Pushfa.unsubscribeTopic("TOPIC_UUID")
let topics = try await Pushfa.getTopics()

// login
try await Pushfa.setExternalId("user-123")
try await Pushfa.addAlias(label: "crm_id", value: "8451")
try await Pushfa.addAlias(label: "mobile", value: "09120000000")

// logout on shared device
try await Pushfa.setExternalId(nil)
try await Pushfa.removeAlias(label: "crm_id")
try await Pushfa.removeAlias(label: "mobile")
```

External ID همان Alias اصلی و یکتا برای Profile است. Custom Alias چند مقدار برچسب‌دار دارد. Subscriber ID شناسه پایدار نصب و گزینه پیشنهادی هدف‌گیری است؛ APNs/FCM Token ممکن است تغییر کند.

## ۷. دریافت سفارشی، Tap و Deep Link

SDK رفتار اعلان را با `notificationHandler` و `notificationTapHandler` قابل‌شخصی‌سازی می‌کند. نام/امضای دقیق Closureها را با README نسخه 1.0.0 تطبیق دهید. هدف معماری:

- Foreground: تصمیم درباره Banner/Sound یا UI درون‌اپ.
- Tap: خواندن URL، Action ID و Additional Data.
- Cold start: Route را تا آماده‌شدن Navigation نگه دارید.
- Universal Link: Associated Domains و فایل AASA معتبر.
- Custom Scheme: URL Types و Route parser امن.

نمونه Router مستقل از SDK:

```swift
@MainActor
final class PushRouter: ObservableObject {
    @Published var pendingRoute: URL?

    func handle(url: URL?) {
        guard let url else { return }
        pendingRoute = url
    }
}
```

URL ورودی را allowlist و validate کنید؛ هیچ command یا WebView حساس را مستقیماً از `additional_data` اجرا نکنید.

## ۸. Payload مرجع iOS

| فیلد | رفتار |
|---|---|
| `title`, `body` | APS alert |
| `url` | Universal Link، Custom Scheme یا Route اپ |
| `image` | Rich attachment از Extension |
| `actions` | Actionهای اعلان و URL هر دکمه |
| `collapse_id` | `apns-collapse-id` برای جایگزینی |
| `silent` | محتوای Background/بدون صدای معمول |
| `additional_data` | داده سفارشی برای App Router/Analytics |
| `ackUrl`, `clickAckUrl` | گزارش Delivery/Click |

محدودیت زمانی Notification Service Extension را رعایت کنید و در `serviceExtensionTimeWillExpire` بهترین محتوای موجود را تحویل دهید.

## ۹. ارسال تکی، گروهی و هدف‌گیری

همه درخواست‌ها از Backend و با Private Key اجرا شوند:

- Token: `POST /api/webservices/send-single-message`
- Subscriber: `POST /api/webservices/send-via-subscriber-id`
- External ID: `POST /api/webservices/send-via-user-alias-id`
- Custom Alias: `POST /api/webservices/send-via-alias`
- گروه یا Topic: `POST /api/webservices/send-group-message`
- وضعیت: `POST /api/webservices/check-notification-status`

نمونه ارسال به iOS:

```json
{
  "api_public_key": "YOUR_PUBLIC_KEY",
  "api_private_key": "YOUR_PRIVATE_KEY",
  "subscriber_ids": ["SUBSCRIBER_UUID"],
  "title": "سفارش ارسال شد",
  "body": "برای مشاهده جزئیات لمس کنید",
  "link_url": "myapp://orders/42",
  "image_url": "https://cdn.example.com/order-shipped.jpg",
  "sendTime": "current",
  "platform": "ios",
  "collapse_id": "order-42",
  "additional_data": {"screen":"order","order_id":42},
  "get_delivery_status": true,
  "get_click_status": true
}
```

ارسال با Custom Alias:

```json
{
  "api_public_key": "YOUR_PUBLIC_KEY",
  "api_private_key": "YOUR_PRIVATE_KEY",
  "label": "crm_id",
  "values": ["8451", "8452"],
  "only_last_device": true,
  "title": "پیام شخصی",
  "body": "یک بروزرسانی برای حساب شما داریم",
  "sendTime": "current",
  "platform": "ios"
}
```

فیلدهای مشترک: `title` حداکثر ۳۵، `body` حداکثر ۱۵۰، `link_url`, `image_url`, دو دکمه، `ttl`, `sendTime`, `time`, `collapse_id`, `silent`, `additional_data`, `use_brackets`, `throttle_rate_per_minute`.

برای Topic، در endpoint گروهی `topic: "all"` یا UUID Topic و `platform: "ios"` بفرستید.

## ۱۰. RetenX در iOS

```swift
try await Pushfa.trackEvent(
    "purchase",
    parameters: [
        "order_id": 9182,
        "amount": 120000
    ]
)
```

پیش از Event بهتر است External ID/Custom Alias کاربر تنظیم شده باشد. نام Start/Exit event در Journey باید دقیقاً برابر نام SDK باشد. RetenX نیازمند Feature فعال و پلن Pro است.

برای Eventهای قطعی مانند Payment، ارسال از Backend مطمئن‌تر است:

- `POST /api/webservices/retention/events`
- `POST /api/webservices/retention/profiles/upsert`

Retry سروری باید idempotency UUID داشته باشد. داده پرداخت یا اطلاعات حساس را بدون نیاز در Event properties نفرستید.

## ۱۱. سرویس Auth Push در iOS

سرویس Auth Push بر Profile/Subscriber دستگاه مورد اعتماد تکیه دارد، اما امنیت Login در Backend کسب‌وکار تکمیل می‌شود:

1. بعد از Login معتبر، Subscriber ID را با `POST /api/webservices/auth-push/bind` به `auth_identity` متصل کنید و `expires_at` را برابر پایان واقعی Session/JWT یا Access Token بفرستید.
2. Backend تلاش ورود جدید را با `POST /api/webservices/auth-push/challenges` آغاز کند.
3. Push Approval/OTP با Deep Link امن در دستگاه مورد اعتماد باز شود.
4. درخواست‌کننده Status را Poll کند.
5. Grant با `POST /api/webservices/auth-push/grants/consume` فقط در Backend مصرف شود.
6. Backend Session/Token خودش را صادر کند.
7. در Logout، حذف دستگاه یا تغییر مالکیت، `unbind` اجرا شود.

`expires_at` در Bind اختیاری و ISO 8601 است؛ برای نمونه `2026-09-07T12:30:00Z`. مقدار را مستقیماً از `exp` توکن ورود یا پایان نشست بسازید و هنگام Refresh Token، Bind را با تاریخ جدید تمدید کنید. بعد از انقضا، دستگاه خودکار از مقصدهای سرویس Auth Push خارج و Challenge/Grant باز مرتبط باطل می‌شود. نبودن این فیلد رفتار قدیمیِ بدون انقضا را حفظ می‌کند؛ Logout، حذف دستگاه یا Revoke زودتر از موعد همچنان نیازمند `unbind` است.

Deep Link تأیید باید Challenge UUID را نمایش دهد، ولی تصمیم نهایی را به داده امضاشده/توکن امن Backend متصل کند. Grant، OTP، Client Token و Private Key در UserDefaults یا Analytics ذخیره نشوند. Face ID/Touch ID می‌تواند پیش از Approval محلی استفاده شود، اما جای Consume سروری Grant را نمی‌گیرد.

## ۱۲. عیب‌یابی

- Token نمی‌آید: Entitlement، Provisioning Profile، APNs Key، Bundle ID و Firebase config را بررسی کنید.
- FCM ثبت است ولی Push نیست: APNs Key در Firebase، Team ID/Key ID و محیط Sandbox/Production را کنترل کنید.
- Foreground نمایش ندارد: `UNUserNotificationCenterDelegate` و `willPresent` را بررسی کنید.
- تصویر نمایش ندارد: Extension، Product `PushfaExtension`، `mutable-content` و URL عمومی HTTPS را کنترل کنید.
- Tap در Cold Start گم می‌شود: Route را تا آماده‌شدن Navigation نگه دارید.
- دو callback یا رفتار متناقض: Firebase swizzling و Delegate دستی هم‌زمان ناقص تنظیم شده‌اند.
- Delivery ناقص است: Extension و Ack background را بررسی کنید؛ محدودیت‌های سیستم iOS را در تحلیل لحاظ کنید.
- ارسال گروهی به پلتفرم دیگر می‌رود: `platform: "ios"` و سرویس درست را انتخاب کنید.

## چک‌لیست Production

- [ ] Bundle ID، Firebase App، APNs Key و Service Account هماهنگ‌اند.
- [ ] Capabilityهای Push و Remote Notifications فعال‌اند.
- [ ] SDK از Tag 1.0.0 و Product درست برای هر Target نصب شده است.
- [ ] Permission بعد از Soft Prompt و روی دستگاه واقعی بررسی شده است.
- [ ] Token refresh، Subscriber ID، Login/Logout و Aliasها درست کار می‌کنند.
- [ ] Foreground، Background، Terminated، Tap و Cold Start بررسی شده‌اند.
- [ ] Universal Link/Custom Scheme allowlist و validate می‌شود.
- [ ] Rich Image و Delivery Extension در زمان محدود درست fallback می‌کنند.
- [ ] Private Key و Service Account داخل Bundle نیستند.
- [ ] RetenX/سرویس Auth Push دارای Backend امن، Feature Gate و revoke هستند.

## دستور آماده برای Agent

```text
براساس فایل راهنمای جامع Pushfa iOS Push، پوش نیتیو اپ را پیاده‌سازی کن.
Bundle ID: ...
Public Key: ...
UI lifecycle: SwiftUI / UIKit
Firebase swizzling: فعال/غیرفعال/نامشخص
Deployment target: ...
Deep links: Universal Link / Custom Scheme و routeها
External ID و Custom Aliasها: ...
Rich Notification: لازم/غیرلازم
RetenX events: ...
سرویس Auth Push: فعال/غیرفعال

ابتدا Package.swift/Xcode targets، AppDelegate، entitlements، Firebase و Navigation موجود را بررسی کن. فقط Public Key را در اپ قرار بده. Notification Service Extension را فقط در صورت نیاز به Rich Image/Delivery اضافه کن. در پایان فایل‌های تغییرکرده، تنظیمات Apple/Firebase/Pushfa و سناریوهای تست دستی روی دستگاه واقعی را گزارش بده.
```

## مراجع

- `https://pushfa.com/index/doc/ios-sdk`
- `https://pushfa.com/index/doc/ios-sdk-integration`
- `https://pushfa.com/index/doc/api-ios-push`
- `https://github.com/pushfa/pushfa-ios-sdk#readme`
