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

> مرجع Native Android برای توسعه‌دهنده و Agent — Pushfa Android SDK v2.0.4

این سند نصب، Permission، ثبت هویت، دریافت Push، ارسال سروری، Alias/Custom Alias، سرویس Auth Push و RetenX را برای اپ Android پوشش می‌دهد. APIهای ارسال میان Web، Android و iOS مشترک‌اند؛ تفاوت اصلی در SDK دریافت‌کننده است.

## امنیت و پیش‌نیاز

- حداقل Android 6 (API 23)، compile SDK 35 و Google Play services برای FCM.
- فقط `api_public_key` داخل اپ قرار می‌گیرد.
- `api_private_key` و Firebase Service Account فقط در Backend/پنل Pushfa باقی می‌مانند.
- `google-services.json` در `app/google-services.json` قرار می‌گیرد و باید متعلق به همان Firebase Project سرویس Pushfa باشد.
- Service Account را هرگز داخل APK، Repository یا پیام پشتیبانی منتشر نکنید.

## ۱. آماده‌سازی Firebase و پنل

1. در Firebase، Android App را با `applicationId` دقیق ثبت کنید.
2. `google-services.json` را در ماژول app قرار دهید.
3. از Project Settings > Service Accounts یک کلید Firebase Admin SDK بسازید.
4. در Pushfa سرویس از نوع Android بسازید و محتوای کامل Service Account را فقط در پنل وارد کنید.
5. Public Key را به توسعه‌دهنده اپ بدهید.

Firebase Web Config برای سرویس Android لازم نیست. Project ID فایل کلاینت و Service Account سرور باید یکسان باشد.

## ۲. نصب SDK

روش پیشنهادی Maven Central:

```kotlin
// settings.gradle.kts
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
    }
}

// app/build.gradle.kts
dependencies {
    implementation("com.pushfa:pushfa-android-sdk:2.0.4")
}
```

روش جایگزین JitPack، بدون Dependency بالا:

```kotlin
maven { url = uri("https://jitpack.io") }
implementation("com.github.pushfa:pushfa-android-sdk:2.0.4")
```

Google Services plugin:

```kotlin
// root build.gradle.kts
plugins {
    id("com.google.gms.google-services") version "4.5.0" apply false
}

// app/build.gradle.kts
plugins {
    id("com.android.application")
    id("com.google.gms.google-services")
}
```

نسخه 2.0.4 اصلاح لینک در Android 12+ و جایگزینی اعلان با Collapse ID یکسان را دارد. Dependency Maven Central و JitPack را هم‌زمان اضافه نکنید.

## ۳. Initialize

```kotlin
class App : Application() {
    override fun onCreate() {
        super.onCreate()

        Pushfa.initialize(
            this,
            PushfaConfig(
                apiPublicKey = "YOUR_PUBLIC_KEY",
                smallIconResId = R.drawable.ic_stat_pushfa,
                notificationChannelId = "marketing",
                notificationChannelName = "Marketing notifications"
            )
        ) { result ->
            if (result.isSuccess) {
                Log.d("Pushfa", "subscriber=${result.value?.subscriberId}")
            } else {
                Log.e("Pushfa", result.error?.message.orEmpty())
            }
        }
    }
}
```

Application را در Manifest ثبت کنید:

```xml
<application android:name=".App" ... />
```

آیکن کوچک Status Bar باید drawable تک‌رنگ مناسب باشد.

## ۴. Permission و Prompt

از Android 13 (API 33) مجوز `POST_NOTIFICATIONS` لازم است. SDK آن را در Manifest ادغام می‌کند، ولی Runtime Prompt باید در زمان معنادار نمایش داده شود. ابتدا یک Soft Prompt یا Bottom Sheet اپ با توضیح ارزش اعلان نشان دهید، سپس:

```kotlin
Pushfa.requestNotificationPermission(this)

override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<out String>,
    grantResults: IntArray
) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    if (requestCode == Pushfa.DEFAULT_PERMISSION_REQUEST_CODE &&
        Pushfa.areNotificationsEnabled(this)) {
        Pushfa.registerForPush { result ->
            Log.d("Pushfa", "registered=${result.isSuccess}")
        }
    }
}
```

قبل از Permission نیز Subscriber ID ساخته می‌شود و Alias، Topic و Event قابل استفاده‌اند، ولی Push فعال بدون Token ثبت‌شده ممکن نیست. در حالت «Don’t ask again» فقط پس از اقدام صریح کاربر لینک تنظیمات اپ را نشان دهید.

## ۵. API عمومی SDK

```kotlin
Pushfa.registerForPush { result -> }
Pushfa.syncState { result -> }

val subscriberId = Pushfa.getSubscriberId()
val token = Pushfa.getPushToken()
val cachedState = Pushfa.currentState()
val enabled = Pushfa.areNotificationsEnabled(context)
```

همه عملیات شبکه asynchronous و callback روی Main Thread است.

## ۶. Topic و هویت مخاطب

```kotlin
Pushfa.subscribeTopic("TOPIC_UUID") { result -> }
Pushfa.unsubscribeTopic("TOPIC_UUID") { result -> }
Pushfa.getTopics { result ->
    val topics = result.value.orEmpty()
}
```

External ID همان Alias اصلی است؛ هر Profile یک External ID دارد. Custom Alias چند شناسه برچسب‌دار است:

```kotlin
// login
Pushfa.setExternalId("USER-123") { result -> }
Pushfa.addAliases(
    mapOf("mobile" to "09120000000", "crm_id" to "CRM-9")
) { result -> }

// profile update
Pushfa.addAlias("tier", "gold") { result -> }
Pushfa.removeAlias("tier") { result -> }
Pushfa.getAliases { result -> }

// logout on a shared device
Pushfa.setExternalId(null) { result -> }
Pushfa.removeAliases(listOf("mobile", "crm_id")) { result -> }
```

مجوز عملیات Alias و Topic در تنظیمات سرویس Pushfa اعمال می‌شود. Subscriber ID را برای هدف‌گیری پایدار ترجیح دهید؛ FCM Token قابل‌تعویض است.

## ۷. دریافت، نمایش و Deep Link

SDK به‌صورت پیش‌فرض اعلان Foreground/Background، تصویر، دو دکمه، Deep Link، Silent، Collapse ID، Additional Data و گزارش Delivery/Click را مدیریت می‌کند.

```kotlin
Pushfa.setNotificationListener { message ->
    analytics.track("push_received", message.additionalData)

    false // نمایش خودکار SDK ادامه یابد
          // true یعنی اپ نمایش را کاملاً برعهده گرفته است
}
```

اگر Listener مقدار `true` دهد، Delivery خودکار ثبت نمی‌شود. برای Renderer استاندارد و Ack از `Pushfa.displayNotification(context, message)` استفاده کنید.

Deep Link:

```kotlin
private fun handlePushfaRoute(intent: Intent) {
    val route = intent.getStringExtra(Pushfa.EXTRA_TARGET_URL)
        ?: intent.data?.path

    when (route) {
        "/promotion" -> openPromotionScreen()
        "/orders" -> openOrdersScreen()
    }
}

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    handlePushfaRoute(intent)
}

override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    handlePushfaRoute(intent)
}
```

`Pushfa.EXTRA_ACTION_ID` مقدار کلیک `body`، `btn-left` یا `btn-right` را مشخص می‌کند. URL کامل و App Link/Custom Scheme با `ACTION_VIEW` باز می‌شود؛ مسیر نسبی اپ را باز و در Intent تحویل می‌شود.

## ۸. اپ دارای FirebaseMessagingService

فقط یک Service باید `MESSAGING_EVENT` را دریافت کند. اگر اپ Service خودش را دارد، Service خودکار Pushfa را با Manifest merge حذف و پیام‌ها را تحویل SDK دهید:

```kotlin
class AppMessagingService : FirebaseMessagingService() {
    override fun onNewToken(token: String) {
        Pushfa.handleNewToken(applicationContext, token)
    }

    override fun onMessageReceived(message: RemoteMessage) {
        if (!Pushfa.handleMessage(applicationContext, message)) {
            // پیام FCM غیر Pushfa
        }
    }
}
```

Service `com.pushfa.sdk.PushfaFirebaseMessagingService` را با `tools:node="remove"` حذف کنید تا دو گیرنده فعال نباشند.

## ۹. Payload مرجع

| کلید | رفتار Android SDK |
|---|---|
| `id` | شناسه گزارش/اعلان |
| `title`, `body` | متن اعلان |
| `url` | لینک یا مسیر کلیک بدنه |
| `image`, `icon` | Big Picture / Large Icon |
| `actions` | دکمه‌ها و URL چپ/راست |
| `ackUrl`, `clickAckUrl` | گزارش با WorkManager و retry |
| `collapse_id` | جایگزینی اعلان قبلی با tag/id مشترک |
| `silent` | بدون صدا و لرزش |
| `additional_data` | JSON سفارشی در `PushfaMessage` |

Delivery پس از پذیرش واقعی توسط NotificationManager صف می‌شود. خطاهای Ack با WorkManager تا بازگشت اینترنت Retry می‌شوند.

## ۱۰. ارسال از Backend

Private Key فقط در Backend. Endpointهای مهم:

| هدف | Endpoint |
|---|---|
| Token تکی | `POST /api/webservices/send-single-message` |
| Subscriber ID | `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` |

نمونه ارسال پایدار به Subscriber:

```json
{
  "api_public_key": "YOUR_PUBLIC_KEY",
  "api_private_key": "YOUR_PRIVATE_KEY",
  "subscriber_ids": ["SUBSCRIBER_UUID"],
  "title": "وضعیت جدید سفارش",
  "body": "سفارش شما ارسال شد",
  "link_url": "/orders/42",
  "sendTime": "current",
  "platform": "android",
  "collapse_id": "order-42",
  "additional_data": {"order_id":42,"screen":"order_detail"},
  "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": ["CRM-9"],
  "only_last_device": true,
  "title": "پیام حساب",
  "body": "اطلاعات جدید آماده است",
  "sendTime": "current",
  "platform": "android"
}
```

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

## ۱۱. RetenX در Android

```kotlin
Pushfa.trackEvent(
    eventName = "product_view",
    params = mapOf(
        "product_id" to 42,
        "price" to 125000
    ),
    aliases = mapOf("mobile" to "09120000000")
) { result ->
    if (!result.isSuccess) Log.e("Pushfa", result.error?.message.orEmpty())
}
```

Event فقط زمانی برای Journey مفید است که نام آن دقیقاً با Start/Exit event پنل هماهنگ باشد. RetenX به فعال‌بودن قابلیت و پلن Pro نیاز دارد. برای Event سروری از `POST /api/webservices/retention/events` و ساخت/به‌روزرسانی Profile از `POST /api/webservices/retention/profiles/upsert` استفاده کنید.

قواعد پیشنهادی:

- نام event به شکل snake_case.
- داده کوچک و مرتبط در params.
- شناسه پایدار در External ID یا Custom Alias.
- idempotency یکتا برای Retryهای Backend.
- Logout نباید Event کاربر قبلی را به Profile جدید وصل کند.

## ۱۲. سرویس Auth Push برای Android

SDK Android هویت و Push دستگاه مورد اعتماد را فراهم می‌کند؛ منطق Challenge/Grant باید در Backend امن اجرا شود:

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

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

هیچ Grant، OTP، Client Token یا Private Key را در اپ یا لاگ دائمی ذخیره نکنید. SMS fallback باید Challenge قبلی را Cancel کند.

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

- `subscriberId` هست ولی Push نیست: Permission، تنظیمات اعلان سیستم، FCM Token و Play services را بررسی کنید.
- `SENDER_ID_MISMATCH`: فایل `google-services.json` و Service Account متعلق به یک Firebase Project نیستند.
- دو اعلان یا دو callback: دو `FirebaseMessagingService` فعال است.
- Deep Link در Android 12+ باز نمی‌شود: SDK را حداقل به 2.0.4 ارتقا دهید و Intent filter را کنترل کنید.
- Collapse جایگزین نمی‌کند: دو پیام جدید با `collapse_id` کاملاً یکسان ارسال و اعلان قدیمی را پاک کنید.
- تصویر نمی‌آید: URL عمومی HTTPS، اندازه تصویر و محدودیت شبکه را بررسی کنید.
- Delivery ثبت نمی‌شود: Listener سفارشی `true` داده ولی `Pushfa.displayNotification` فراخوانی نشده است.

## چک‌لیست Production

- [ ] Firebase Client و Service Account یک Project دارند.
- [ ] فقط SDK 2.0.4 از یک Repository نصب شده است.
- [ ] Permission Android 13+ بعد از Soft Prompt درخواست می‌شود.
- [ ] Subscriber ID و Token Refresh درست همگام می‌شوند.
- [ ] Login/Logout و پاک‌سازی Aliasها پیاده شده است.
- [ ] Foreground، Background، اپ بسته، دکمه‌ها و Deep Link دستی بررسی شده‌اند.
- [ ] Delivery/Click، Offline retry، Collapse، Silent و Additional Data بررسی شده‌اند.
- [ ] Private Key و Service Account داخل اپ نیستند.
- [ ] RetenX/سرویس Auth Push فقط با Backend امن و Feature Gate فعال شده‌اند.

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

```text
براساس فایل راهنمای جامع Pushfa Android Push، پوش نیتیو اپ را پیاده‌سازی کن.
Package/Application ID: ...
Public Key: ...
معماری UI: Views / Compose
FirebaseMessagingService موجود: بله/خیر
Deep-link routes: ...
External ID و Custom Aliasها: ...
RetenX events: ...
سرویس Auth Push: فعال/غیرفعال

ابتدا Gradle، Manifest، Application، Firebase و Navigation فعلی را بررسی کن. فقط Public Key را در اپ بگذار و Private Key را در Backend نگه دار. قرارداد Delivery/Click و Token Refresh را حفظ کن و در پایان تغییرات اپ، تنظیمات Firebase/پنل و سناریوهای تست دستی را گزارش بده.
```

## مراجع

- `https://pushfa.com/index/doc/android-sdk`
- `https://pushfa.com/index/doc/android-sdk-methods`
- `https://pushfa.com/index/doc/android-receive-push`
- `https://pushfa.com/index/doc/android-troubleshooting`
- `https://github.com/pushfa/pushfa-android-sdk#readme`
