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

> نسخه مرجع برای توسعه‌دهنده و عامل هوشمند (Agent) — منطبق با پیاده‌سازی جاری Pushfa در اوت ۲۰۲۶

این سند برای تحویل مستقیم به توسعه‌دهنده یا Agent نوشته شده است. با دادن این فایل، آدرس سایت، کلید عمومی سرویس و شرح کوتاه سناریو، باید بتوان Web Push، Prompt، هویت مخاطب، ارسال، سرویس Auth Push و RetenX را بدون حدس‌زدن قراردادهای Pushfa پیاده‌سازی کرد.

## قواعد قطعی امنیتی

- `api_public_key` برای مرورگر عمومی است.
- `api_private_key` فقط در Backend یا Secret Manager نگهداری می‌شود؛ هرگز در HTML، JavaScript، Service Worker، اپ موبایل، Git یا لاگ قرار نگیرد.
- دامنه باید HTTPS باشد و Service Worker در scope ریشه دامنه نصب شود.
- Alias، Topic، Token و Subscriber باید متعلق به همان سرویس باشند.
- لینک‌ها و دکمه‌های اعلان را از مسیر استاندارد Pushfa عبور دهید تا Delivery/Click Report از بین نرود.

## معماری و مفاهیم

| مفهوم | کاربرد |
|---|---|
| Service | پروژه مستقل Pushfa با Public/Private Key و Firebase config |
| Subscriber ID | UUID پایدار پروفایل مخاطب؛ شناسه پیشنهادی برای هدف‌گیری |
| FCM Token | شناسه فنی و قابل‌تعویض مرورگر؛ هویت کسب‌وکاری نیست |
| External ID / Alias | شناسه اصلی کاربر واردشده، مانند `user-123`؛ هر پروفایل یک مقدار دارد |
| Custom Alias | چند شناسه برچسب‌دار، مانند `mobile`, `crm_id`, `email` |
| Topic | گروه عضویت اختیاری مخاطب |
| Bracket | ویژگی شخصی‌سازی پیام، مانند `first_name` یا `plan` |
| RetenX Event | رفتار کاربر که Profile/Journey را تغذیه می‌کند |

Subscriber ID را محور اتصال داده‌ها قرار دهید. Token ممکن است Refresh شود، ولی Subscriber ID باید باقی بماند. Profile می‌تواند قبل از گرفتن اجازه Push ساخته شود و بعداً Token به همان Profile متصل شود.

## ۱. ساخت سرویس در پنل

1. در پنل Pushfa یک سرویس از نوع Web/PWA بسازید.
2. دامنه دقیق HTTPS را ثبت کنید.
3. Firebase عمومی Pushfa یا Firebase اختصاصی خود را انتخاب کنید.
4. در حالت اختصاصی، Firebase Web Config و Service Account متعلق به یک Firebase Project باشند.
5. تنظیمات Prompt، Alias permissions، Topic permissions و گزارش‌ها را تعیین کنید.
6. `api_public_key` را برای Frontend و `api_private_key` را فقط برای Backend بردارید.

تغییر تنظیمات SDK ممکن است تا پاک‌شدن Cache اسکریپت تولیدی دیده نشود؛ در خود Pushfa این Cache باید با `ServiceManager::clearServiceCache()` باطل شود.

## ۲. نصب SDK نسل دوم

پیشنهاد استاندارد:

```html
<script
  src="https://sdk.pushfa.com/notification-v2.js?api_public_key=YOUR_PUBLIC_KEY"
  type="module">
</script>
```

برای کنترل کامل زمان Prompt:

```html
<script
  src="https://sdk.pushfa.com/notification-v2.js?api_public_key=YOUR_PUBLIC_KEY&manual_prompt=1"
  type="module">
</script>
```

در نصب روی دامنه اصلی Pushfa ممکن است URL برابر `https://pushfa.com/notification-v2.js` باشد. Base URL محیط را یک‌جا قابل‌تنظیم نگه دارید.

SDK فایل‌های Firebase و Service Worker را هماهنگ می‌کند. فایل Worker باید در ریشه دامنه با نام مورد انتظار نصب شود تا همه صفحات را پوشش دهد. نسخه دانلودی Worker، `dynamic.js?api_public_key=...` را import می‌کند.

## ۳. Promptهای آماده، مستقیم و سفارشی

### Prompt آماده Pushfa

در پنل یکی از Styleهای آماده را انتخاب کنید. SDK ابتدا Soft Prompt برندشده را نشان می‌دهد و پس از قبول کاربر، Permission واقعی مرورگر را درخواست می‌کند. متن، رنگ، فونت، آیکن و فاصله نمایش مجدد از تنظیمات سرویس می‌آید.

```js
await window.Pushfa.showPrompt();
```

### Prompt مستقیم مرورگر

با `prompt_style=default_browser` واسط آماده Pushfa حذف و Permission مرورگر مستقیماً نمایش داده می‌شود. این کار را فقط پس از یک اقدام روشن کاربر انجام دهید:

```js
document.querySelector('#enable-push').addEventListener('click', async () => {
  const result = await window.Pushfa.requestPermission();
  console.log(result);
});
```

### Prompt کاملاً سفارشی

در پنل `custom_soft_prompt` را انتخاب کنید. Pushfa UI نمی‌سازد و رویداد زیر را منتشر می‌کند:

```html
<div id="my-soft-prompt" hidden>
  <p>از وضعیت سفارش و تخفیف‌های مرتبط باخبر شوید.</p>
  <button id="my-soft-prompt-accept" type="button">فعال‌سازی</button>
  <button id="my-soft-prompt-later" type="button">بعداً</button>
</div>

<script>
window.addEventListener('pushfaCustomPromptRequest', () => {
  document.querySelector('#my-soft-prompt').hidden = false;
});

document.querySelector('#my-soft-prompt-accept').onclick = async () => {
  await window.Pushfa.requestPermission();
  document.querySelector('#my-soft-prompt').hidden = true;
};

document.querySelector('#my-soft-prompt-later').onclick = () => {
  window.Pushfa.snoozePrompt?.();
  document.querySelector('#my-soft-prompt').hidden = true;
};
</script>
```

### دکمه هوشمند و وضعیت فعلی

برای CTA اختصاصی، `manual_prompt=1` بگذارید و پس از کلیک وضعیت را بررسی و Permission را درخواست کنید. نام‌های عمومی سازگار را تغییر ندهید: `pushfaButton`, `shouldShowPushfaButton`, `getPushfaToken`.

### iOS Web Push/PWA

iOS Web Push با اپ نیتیو iOS متفاوت است. در iOS 16.4+ کاربر ابتدا باید سایت را از Safari با **Add to Home Screen** نصب و از آیکن Home Screen باز کند. سپس Permission درخواست شود:

```js
window.Pushfa.showIosInstallPrompt?.();
```

عنوان راهنمای نصب از `ios_install_prompt_title` می‌آید. روی Tab عادی Safari انتظار Token نداشته باشید.

## ۴. API عمومی مرورگر

نام دقیق متدها را از نسخه SDK محیط بررسی کنید. قراردادهای اصلی نسل دوم:

```js
const subscriberId = await window.Pushfa.getSubscriberId?.();
const token = await window.Pushfa.getToken?.();

await window.Pushfa.subscribeTopic('TOPIC_UUID');
await window.Pushfa.unsubscribeTopic('TOPIC_UUID');
const topics = await window.Pushfa.getTopics();

await window.Pushfa.setExternalId('USER-123');
await window.Pushfa.setExternalId(null); // logout

await window.Pushfa.addAlias('mobile', '09120000000');
await window.Pushfa.addAliases({ crm_id: 'CRM-42', tier: 'gold' });
await window.Pushfa.removeAlias('tier');
const aliases = await window.Pushfa.getAliases();
```

نام‌های قدیمی عمومی که برای سازگاری مشتریان نباید شکسته شوند:

`subscribeTopic`, `unSubscribeTopic`, `getDeviceTopics`, `getUserAliasID`, `setUserAliasID`, `PushfaRetenXEvent`, `pushfaButton`, `shouldShowPushfaButton`, `getPushfaToken`.

مجوز Set/Get/Delete Alias و Custom Alias در تنظیمات همان سرویس کنترل می‌شود. برای عملیات حساس، Backend API دارای Private Key را ترجیح دهید.

## ۵. الگوی Login و Logout

پس از Login موفق کسب‌وکار:

```js
await window.Pushfa.setExternalId(currentUser.id);
await window.Pushfa.addAliases({
  crm_id: currentUser.crmId,
  mobile: currentUser.mobile
});
```

در Logout، Alias اصلی را پاک کنید تا مرورگر مشترک به کاربر قبلی متصل نماند. درباره Custom Aliasها نیز سیاست صریح داشته باشید:

```js
await window.Pushfa.setExternalId(null);
await window.Pushfa.removeAliases(['crm_id', 'mobile']);
```

## ۶. Topic و Bracket

Topic برای عضویت گروهی و Bracket برای شخصی‌سازی است. UUID Topic را استفاده کنید، نه عنوان نمایشی آن.

Backend endpoints:

- `POST /api/webservices/topics/create`
- `POST /api/webservices/topics/subscribe`
- `POST /api/webservices/topics/unsubscribe`
- `POST /api/webservices/topics/user-topics`
- `POST /api/webservices/topics/topic-members`
- `POST /api/webservices/brackets/add`
- `POST /api/webservices/brackets/remove`
- `POST /api/webservices/brackets/get`

قالب شخصی‌سازی پیام:

- `{first_name}`
- `{first_name:کاربر عزیز}`
- `{custom_alias:crm_id}`
- `{custom_alias:tier:standard}`

برای فعال‌کردن جایگزینی در ارسال، `use_brackets: true` بفرستید.

## ۷. ارسال از Backend

Base URL نمونه‌ها: `https://pushfa.com/api`. تمام endpointهای `/webservices` نیازمند `api_public_key` و `api_private_key` هستند.

### ارسال تکی به Token

`POST /api/webservices/send-single-message`

```bash
curl -X POST https://pushfa.com/api/webservices/send-single-message \
  -H "Content-Type: application/json" \
  -d '{
    "api_public_key":"YOUR_PUBLIC_KEY",
    "api_private_key":"YOUR_PRIVATE_KEY",
    "fcm_token":"FCM_TOKEN",
    "title":"سفارش ارسال شد",
    "body":"برای مشاهده وضعیت کلیک کنید",
    "link_url":"https://example.com/orders/42",
    "sendTime":"current",
    "get_delivery_status":true,
    "get_click_status":true,
    "additional_data":{"type":"order","order_id":42}
  }'
```

### ارسال به Subscriber ID پایدار

`POST /api/webservices/send-via-subscriber-id`

```json
{
  "api_public_key": "YOUR_PUBLIC_KEY",
  "api_private_key": "YOUR_PRIVATE_KEY",
  "subscriber_ids": ["SUBSCRIBER_UUID"],
  "title": "پیام اختصاصی",
  "body": "متن پیام",
  "sendTime": "current",
  "platform": "all"
}
```

### ارسال با Alias اصلی

`POST /api/webservices/send-via-user-alias-id`

فیلد اصلی `alias` است. `only_last_device=true` فقط آخرین دستگاه و `smart_targeting=true` دستگاه مناسب را هدف می‌گیرد.

### ارسال با Custom Alias

`POST /api/webservices/send-via-alias`

```json
{
  "api_public_key": "YOUR_PUBLIC_KEY",
  "api_private_key": "YOUR_PRIVATE_KEY",
  "label": "crm_id",
  "values": ["CRM-42", "CRM-77"],
  "only_last_device": false,
  "title": "پیام حساب کاربری",
  "body": "جزئیات جدید آماده است",
  "sendTime": "current",
  "platform": "all"
}
```

حداکثر ۱۰۰ مقدار Custom Alias در هر درخواست پذیرفته می‌شود.

### ارسال گروهی و Topic

`POST /api/webservices/send-group-message`

گیرنده یکی از این دو حالت است:

- `fcm_tokens: [...]`
- `topic: "all"` یا UUID Topic

برای محدودسازی از `platform: android|ios|desktop|all` استفاده کنید. `device` فیلتر نوع دستگاه قدیمی/مشترک است و با platform اشتباه نشود.

### ارسال Lite

- `POST /api/webservices/send-single-lite-message`
- `POST /api/webservices/send-group-lite-message`

Lite برای سناریوی سبک‌تر و بدون هزینه/جزئیات کامل گزارش مطابق پلن استفاده می‌شود. برای گزارش Delivery/Click از endpoint معمولی استفاده کنید.

### فیلدهای مشترک پیام

| فیلد | قرارداد |
|---|---|
| `title` | اجباری، حداکثر ۳۵ کاراکتر |
| `body` | اجباری، حداکثر ۱۵۰ کاراکتر |
| `link_url` | URL معتبر یا مسیر پشتیبانی‌شده |
| `image_url` | URL عمومی HTTPS |
| `btn_left`, `btn_right` | آبجکت شامل `title` و `url` |
| `sendTime` | `current` برای فوری یا `delay` برای زمان‌بندی |
| `time` | برای delay با قالب `Y-m-d H:i` |
| `ttl` | زمان اعتبار پیام |
| `collapse_id` | حداکثر ۱۰۰ کاراکتر؛ جایگزینی پیام هم‌نوع |
| `silent` | اعلان بی‌صدا |
| `additional_data` | JSON object سفارشی |
| `throttle_rate_per_minute` | ۱ تا ۱٬۰۰۰٬۰۰۰ |

وضعیت ارسال: `POST /api/webservices/check-notification-status`.

## ۸. RetenX

RetenX نیازمند فعال‌بودن قابلیت توسط مدیر و اشتراک Pro است. Journey با Event شروع می‌شود و می‌تواند Wait، Wait Until، Condition، A/B Split، Push، SMS، Push-to-SMS Fallback، Webhook و End داشته باشد.

### Event از مرورگر

```js
await window.Pushfa.event('add_to_cart', {
  product_id: 123,
  product_name: 'کفش ورزشی',
  price: 480000
}, {
  mobile: '09120000000',
  crm_id: 'CRM-42'
});
```

نام قدیمی سازگار: `window.PushfaRetenXEvent('add_to_cart', params, aliases)`.

### Profile و Event از Backend

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

Backend می‌تواند مخاطب را با `subscriber_id`، `external_id` یا Custom Alias شناسایی کند. برای Retry رویدادهای سروری، idempotency UUID یکتا بفرستید. نام Eventها را snake_case و ثابت نگه دارید؛ مانند `add_to_cart`, `checkout_completed`, `subscription_expired`.

الگوی سبد رهاشده:

`add_to_cart → Wait Until(checkout_completed) → expired → Push → Wait → Push-to-SMS Fallback → End`

همه مسیرها باید به End برسند، مجموع Split باید ۱۰۰٪ باشد و Start/Exit event دقیقاً با نام ارسالی SDK یکسان باشد.

## ۹. سرویس Auth Push

سرویس Auth Push یک ورود بین‌دستگاهی امن است و باید فقط زمانی پیاده شود که قابلیت در سامانه فعال باشد.

### جریان امن

1. دستگاه مورد اعتماد Push permission و Subscriber ID دارد.
2. Backend بعد از Login معتبر دستگاه را Bind می‌کند و `expires_at` را برابر پایان اعتبار همان Session یا Access Token می‌فرستد.
3. صفحه Login جدید از Backend درخواست Challenge می‌کند.
4. Pushfa روی دستگاه مورد اعتماد Approval یا OTP می‌فرستد.
5. کلاینت با `status` Poll می‌کند.
6. پس از Approval، Grant یک‌بارمصرف فقط در Backend مصرف می‌شود.
7. Backend خود کسب‌وکار Session/Access Token را صادر می‌کند.

Backend endpoints:

- `POST /api/webservices/auth-push/bind`
- `POST /api/webservices/auth-push/unbind`
- `POST /api/webservices/auth-push/challenges`
- `POST /api/webservices/auth-push/grants/consume`

نمونه Bind با انقضای داینامیک:

```json
{
  "api_public_key": "YOUR_PUBLIC_KEY",
  "api_private_key": "YOUR_PRIVATE_KEY",
  "subscriber_id": "DEVICE_SUBSCRIBER_UUID",
  "auth_identity": "INTERNAL_USER_UUID",
  "expires_at": "2026-09-07T12:30:00Z"
}
```

`expires_at` اختیاری و به فرمت ISO 8601 است. آن را از `exp` واقعی JWT یا `expires_at` نشست/Access Token بسازید، نه از یک عدد ثابت مستقل. پس از این زمان بایند دیگر برای Challenge قابل استفاده نیست، درخواست‌ها و Grantهای باز مرتبط باطل می‌شوند و رکورد منقضی پاک می‌شود. اگر این فیلد ارسال نشود، برای سازگاری با نسخه‌های قبلی بایند بدون انقضا باقی می‌ماند. در Logout یا Revoke زودتر از موعد همچنان `unbind` را اجرا کنید.

Client endpoints:

- `POST /api/auth-push/status`
- `POST /api/auth-push/verify-otp`
- `POST /api/auth-push/cancel`

ساخت Challenge نمونه:

```json
{
  "api_public_key": "YOUR_PUBLIC_KEY",
  "api_private_key": "YOUR_PRIVATE_KEY",
  "auth_identity": "INTERNAL_USER_UUID",
  "requester_subscriber_id": "LOGIN_BROWSER_SUBSCRIBER_UUID",
  "mode": "approval",
  "expires_in": 120,
  "reference": "INTERNAL_USER_UUID",
  "context": {"ip":"203.0.113.10","device":"Chrome / Windows"}
}
```

Grant فقط یک بار، قبل از `expires_at` و برای همان `auth_identity` مصرف شود. Challenge را در Backend به تلاش Login و کاربر متصل کنید، بعد از موفقیت Session را regenerate کنید و هیچ OTP، Client Token، Grant یا Private Key را لاگ نکنید. اگر کاربر SMS fallback را انتخاب کرد، ابتدا Challenge را Cancel کنید.

## ۱۰. Payload و Service Worker

کلیدهای مصرفی Worker/SDK را تغییر نام ندهید:

`id`, `title`, `body`, `url`, `image`, `icon`, `badge`, `ackUrl`, `actions`, `btnLeftUrl`, `btnRightUrl`, `collapse_id`, `silent`, `additional_data`, `validate_only`.

Worker نمایش Background، کلیک بدنه، URL دکمه‌ها و Ack تحویل را انجام می‌دهد. Foreground توسط Firebase init/SDK مدیریت می‌شود. اگر Renderer سفارشی می‌سازید، Ack تحویل و کلیک و Retry آفلاین را حفظ کنید.

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

- Permission برابر denied: فقط راهنمای تنظیمات مرورگر نشان دهید؛ درخواست مکرر نکنید.
- Token ساخته نمی‌شود: HTTPS، دامنه، VAPID/Firebase config، Service Worker scope و Console را بررسی کنید.
- پیام نمی‌رسد: یکسان‌بودن Firebase Project کلاینت و Service Account، اعتبار Token و وضعیت Worker را بررسی کنید.
- کلیک ثبت نمی‌شود: مسیر مستقیم سفارشی نباید redirect/ack استاندارد Pushfa را دور بزند.
- Alias/Topic خطای 403/422 دارد: مجوز سرویس، مالکیت شناسه و قالب ورودی را بررسی کنید.
- iPhone روی Safari عضو نمی‌شود: نصب PWA روی Home Screen و iOS 16.4+ را کنترل کنید.
- تنظیم جدید دیده نمی‌شود: Cache اسکریپت تولیدی سرویس باید باطل شود.

## ۱۲. چک‌لیست تحویل Production

- [ ] Public Key در Frontend و Private Key فقط در Backend است.
- [ ] SDK با `manual_prompt` متناسب با UX نصب شده است.
- [ ] Prompt فقط پس از نمایش ارزش واضح یا اقدام کاربر ظاهر می‌شود.
- [ ] Service Worker ریشه، Scope، HTTPS و Update آن بررسی شده است.
- [ ] Subscriber ID ذخیره و Token Refresh به همان Profile وصل می‌شود.
- [ ] Login/Logout، External ID و Custom Aliasها سیاست روشن دارند.
- [ ] Foreground، Background، صفحه بسته، دکمه‌ها و Deep Link تست دستی شده‌اند.
- [ ] Delivery/Click، Collapse، Silent و Additional Data بررسی شده‌اند.
- [ ] Eventهای RetenX و Exit eventها دقیق و idempotent هستند.
- [ ] سرویس Auth Push دارای rate limit، fallback، cancel، consume یک‌باره و session regeneration است.

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

این متن را همراه فایل به Agent بدهید:

```text
براساس فایل راهنمای جامع Pushfa Web Push، Web Push این پروژه را پیاده‌سازی کن.
دامنه: https://example.com
Public Key: ...
Backend stack: ...
Prompt مطلوب: آماده / مستقیم / سفارشی
هویت کاربر: external_id=... و custom aliases=...
سناریوی ارسال یا RetenX: ...
سرویس Auth Push: فعال/غیرفعال

Private Key را فقط از متغیر محیطی Backend بخوان. قبل از تغییر، معماری موجود و Service Worker فعلی را بررسی کن. قرارداد عمومی Pushfa، گزارش کلیک/تحویل و رفتار Login/Logout را حفظ کن و در پایان فایل‌های تغییرکرده و مراحل تنظیم پنل را گزارش بده.
```

## مراجع آنلاین رسمی Pushfa

- مستندات Web SDK: `https://pushfa.com/index/doc/sdk-v2`
- متدهای SDK: `https://pushfa.com/index/doc/sdk-methods`
- Promptها: `https://pushfa.com/index/doc/prompts`
- Alias: `https://pushfa.com/index/doc/alias`
- RetenX: `https://pushfa.com/index/doc/retenx`
- سرویس Auth Push: `https://pushfa.com/index/doc/auth-push`
