Live Activity — mobil entegrasyon
iOS Live Activity ve push tetikleyicileri
Scheduled Live Activity Mobile Entegrasyonu
Migration rehberi
Yapılandırma sırası için Migration şablonu. Yeni düzenlemelerde Amaç → Önkoşullar → Endpoint → Request/Response → Hata kodları → Client adımları → İlgili sayfalar bölümlerini tercih edin.
Bu doküman, planli yayinlar için creator ve guest kullanıcılarına yayin baslamadan 60 dakika once backend tarafından tetiklenen Live Activity countdown akisinin mobil entegrasyonunu açıklar.
Kapsam:
- Yalnizca iOS Live Activity entegrasyonu
- Backend-driven remote start
- Widget tarafında local countdown render edilmesi
Bu akista backend start event'i yollar ve runtime token kaydi yapildiktan sonra gerekli durumlarda end event'i de gönderebilir. Arada periyodik update gönderilmez. Countdown tamamen mobilde scheduledStartAt uzerinden hesaplanır.
Ozet
Akis su sekilde calisir:
- Mobil uygulama FCM token,
deviceIdve ActivityKitpush-to-starttoken bilgisini backend'e kaydeder. - Backend, yayin planliysa
plannedStartDate - 60 dakikaanina job schedule eder. - Zaman geldiginde backend creator ve guest kullanıcılarinin uygun iOS cihazlarina remote-start Live Activity push'u yollar.
- iOS sistemi Live Activity'yi baslatir.
- Mobil uygulama runtime
liveActivityTokenbilgisini backend'e kaydeder. - Mobil uygulama countdown UI'ini lokal olarak
scheduledStartAtuzerinden render eder. - Stream
cancelled/ready/started/expiredoldugunda backend runtime token uzerindenendpush gönderebilir.
Minimum Gereksinimler
Bu backend akisi remote-start kullandigi için mobil tarafta efektif minimum sürüm iOS 17.2 olarak düşünülmelidir. Firebase'in resmi dokümanı da remote-start için iOS 17.2 gerektigini belirtir. Live Activity UI kabiliyeti daha eski sürümlerde bulunsa da bu backend akisi o cihazlarda otomatik baslatma yapmaz.
Zorunlu gereksinimler:
- iOS target'inda
ActivityKit - Push Notifications capability
- Live Activities capability
- Info.plist içinde
NSSupportsLiveActivities = YES - FCM Apple platform kurulumu
- Backend'in gönderdigi bundle/Firebase konfigurasyonu ile ayni iOS uygulama hedefi
Not:
- Uygulama React Native veya Flutter olsa bile Live Activity kismi native iOS katmaninda implement edilmelidir.
- Shared katman sadece backend API cagrilarini ve deep link routing'i yonetebilir.
Backend Tarafi Kontrat
1. Device kaydi
Endpoint: POST /notifications/device-token
Auth: Opsiyonel, ancak creator/guest eslesmesi için kullanıcı login durumunda gönderilmelidir.
Request body:
{
"token": "<fcm_token>",
"platform": "ios",
"deviceId": "<stable_device_id>",
"timezone": "Europe/Istanbul",
"liveActivityPushToStartToken": "<activitykit_push_to_start_token>"
}Alanlar:
token: FCM registration tokenplatform: bu akis içiniosdeviceId: cihaz bazli sabit kimlik, ayni cihaz için degismemelitimezone: opsiyonelliveActivityPushToStartToken: ActivityKitpush-to-starttoken
Kurallar:
liveActivityPushToStartTokengönderiliyorsadeviceIdzorunludur.deviceIdher zaman ayni fiziksel cihaz için stabil kalmalidir.deviceIdcihaz modeli olmamalidir. Ornek olarakiphone-15-pro-maxyerine install/device bazli stabil bir UUID benzeri deger kullanılmalıdır.- Kullanıcı login olduktan sonra bu endpoint tekrar cagrilmalidir; aksi halde cihaz kaydi kullanıcıyla iliskilenmeyebilir.
Sadece push-to-start token guncellemek için mevcut cihaz kaydi varsa su body de gönderilebilir:
{
"platform": "ios",
"deviceId": "<stable_device_id>",
"liveActivityPushToStartToken": "<new_push_to_start_token>"
}Response:
{
"success": true
}2. Runtime session kaydi
Endpoint: POST /notifications/live-activity-sessions
Auth: Zorunlu
Request body:
{
"streamId": "65f000000000000000000001",
"deviceId": "<stable_device_id>",
"liveActivityToken": "<runtime_live_activity_token>",
"activityId": "optional-activity-id"
}Alanlar:
streamId: ilgili scheduled stream kimligideviceId: daha once device-token kaydinda kullanilan ayni cihaz kimligiliveActivityToken: remote-start sonrasi ActivityKit tarafından verilen runtime tokenactivityId: opsiyonel local activity identifier
Response:
{
"success": true
}3. Runtime session cleanup
Endpoint: DELETE /notifications/live-activity-sessions/:streamId/:deviceId
Auth: Zorunlu
Bu endpoint, mobil uygulama local olarak dismiss edilen veya manuel kapatilan activity için backend kaydini temizlemek amaciyla kullanılır.
Ne zaman cagrilmali:
- Kullanıcı Live Activity'yi local olarak dismiss ederse
- Mobil taraf activity'yi kendi karariyla manuel olarak sonlandirirsa
- Uygulama acilisinda local tarafta activity artık yok ama backend session kaydi oldugunu dusundugunuz bir reconciliation akisi varsa
Ne zaman cagrilmamali:
- Backend
endpush gönderip activity'yi kapatiyorsa ekstra cleanup çağrısı zorunlu değildir - Her app launch'ta kosulsuz cagrilmamalidir
Ornek request:
DELETE /notifications/live-activity-sessions/65f000000000000000000001/<stable_device_id>
Authorization: Bearer <access_token>Beklenen response:
204 No Content
Notlar:
deviceId,POST /notifications/device-tokenvePOST /notifications/live-activity-sessionscagrilarinda kullandiginiz ayni stabil cihaz kimligi olmalidir- Bu endpoint sadece backend'deki runtime session kaydini temizler; local ActivityKit kapatma islemi mobil tarafta ayrica yapilmalidir
ActivityKit Model Sozlesmesi
Backend, start payload'inda attributes-type olarak sabit su string'i kullanir:
AllmineLiveActivityAttributesiOS tarafındaki ActivityAttributes tipi bu adla eslesmelidir. Tip adi değişirse backend de güncellenmelidir.
Attributes payload
Start aninda gelen attributes:
{
"streamId": "65f000000000000000000001",
"creatorName": "Selin Kaya • Mert Demir",
"streamTitle": "Yayin basligi",
"title": "Yayin basligi",
"role": "creator",
"deepLink": "allmine://live-stream/65f000000000000000000001"
}Content state payload
Start aninda gelen content-state:
{
"streamId": "65f000000000000000000001",
"creatorName": "Selin Kaya • Mert Demir",
"streamTitle": "Yayin basligi",
"statusText": "Yayina kalan sure",
"subtitle": "Yayina katilmak için dokun",
"viewerCount": 128,
"deepLinkUrl": "allmine://home",
"updatedAt": 1776384000,
"title": "Yayin basligi",
"role": "creator",
"deepLink": "allmine://live-stream/65f000000000000000000001",
"scheduledStartAt": 1776708000,
"displayState": "countdown"
}End aninda backend tarafından gelebilecek content-state ornegi:
{
"streamId": "65f000000000000000000001",
"statusText": "Yayin sona erdi",
"subtitle": "Ana sayfaya donmek için dokun",
"viewerCount": 128,
"deepLink": "allmine://home",
"deepLinkUrl": "allmine://home",
"updatedAt": 1776387600,
"displayState": "ended",
"endedReason": "cancelled"
}Swift model onerisi
scheduledStartAt backend tarafında epoch seconds integer olarak gönderilir. Bu nedenle ContentState içinde bunu Int? olarak tutup widget/render katmaninda Date(timeIntervalSince1970:) ile çevirmek uygun yaklasimdir.
import ActivityKit
struct AllmineLiveActivityAttributes: ActivityAttributes {
public struct ContentState: Codable, Hashable {
var streamId: String
var creatorName: String?
var streamTitle: String?
var statusText: String?
var subtitle: String?
var viewerCount: Int?
var deepLinkUrl: String?
var updatedAt: Int?
var title: String?
var role: String?
var deepLink: String?
var scheduledStartAt: Int?
var displayState: DisplayState
var endedReason: EndedReason?
}
var streamId: String
var creatorName: String
var streamTitle: String
var title: String?
var role: String?
var deepLink: String
}
enum DisplayState: String, Codable, Hashable {
case countdown
case ended
}
enum EndedReason: String, Codable, Hashable {
case ready
case started
case cancelled
case expired
}Notlar:
streamIdhemattributeshemcontent-stateiçinde gelir; activity ve stream eslesmesi içinattributes.streamIdkullanmak en temiz yaklasimdir.- Backend
countdownveendedstate'lerini gönderebilir. creatorName, backend tarafında creator ve guest display name'lerinin tek string halınde birleştirilmiş halidir.statusText, countdown numeriginden ayri bir sabit label olarak gösterilmelidir.streamTitle,subtitle,viewerCount,deepLinkUrlveupdatedAtalanlari mobil widget'in mevcut decode ihtiyaci için gönderilir.
iOS Tarafinda Uygulanacak Akis
1. Uygulama acilisinda observer'lari baslat
Remote-start akisinin calismasi için pushToStartTokenUpdates akisina abone olunmalidir.
Onerilen bootstrap:
import ActivityKit
final class LiveActivityBootstrapper {
func start() {
observePushToStartToken()
}
private func observePushToStartToken() {
guard #available(iOS 17.2, *) else { return }
Task {
if let token = Activity<AllmineLiveActivityAttributes>.pushToStartToken {
await registerPushToStartToken(token)
}
for await token in Activity<AllmineLiveActivityAttributes>.pushToStartTokenUpdates {
await registerPushToStartToken(token)
}
}
}
}Yukaridaki akis framework-agnostic düşünülmelidir. React Native veya Flutter kullaniyorsaniz bu kisim native iOS target'inda calismalidir.
2. Push-to-start token'i backend'e kaydet
Activity<AllmineLiveActivityAttributes>.pushToStartToken veya pushToStartTokenUpdates ile gelen Data hex string'e cevrilip POST /notifications/device-token endpoint'ine gönderilmelidir.
Onerilen helper:
func hexString(from data: Data) -> String {
data.map { String(format: "%02x", $0) }.joined()
}Kayit sirasinda su veriler her zaman birlikte gönderilmelidir:
token: FCM tokenplatform:iosdeviceId: sabit cihaz kimligiliveActivityPushToStartToken: hex string token
Pratik oneriler:
- FCM token degistiginde tekrar gönderin.
push-to-starttoken degistiginde tekrar gönderin.- Login sonrasi tekrar gönderin.
- App cold start'ta tekrar göndermek zararli değildir; backend idempotent sekilde gunceller.
3. Runtime token'i backend'e kaydet
Remote-start ile baslayan activity'nin runtime token'i backend'e gönderilmelidir. Bu adim olmadan backend end push'unu gönderemez.
Onerilen akis:
guard #available(iOS 17.2, *) else { return }
Task {
for await activity in Activity<AllmineLiveActivityAttributes>.activityUpdates {
for await token in activity.pushTokenUpdates {
await registerLiveActivitySession(
streamId: activity.attributes.streamId,
deviceId: currentDeviceId,
liveActivityToken: hexString(from: token),
activityId: activity.id
)
}
}
}Bu kayit POST /notifications/live-activity-sessions endpoint'ine gönderilmelidir.
3.1 Local dismiss veya manual end oldugunda cleanup gönder
Runtime session kaydi yapildiysa, activity local tarafta kullanıcı veya uygulama tarafından kapatildiginda backend session kaydi da temizlenmelidir.
Onerilen akis:
guard #available(iOS 17.2, *) else { return }
Task {
for await activity in Activity<AllmineLiveActivityAttributes>.activityUpdates {
for await state in activity.activityStateUpdates {
if state == .dismissed {
await deleteLiveActivitySession(
streamId: activity.attributes.streamId,
deviceId: currentDeviceId
)
}
}
}
}Uygulama kendi tarafında explicit activity.end(...) çağırıyorsa, ayni cleanup request'i bu akisin ardindan da gönderilebilir.
4. Widget countdown'u lokal hesapla
Backend dakikalik update göndermedigi için widget countdown'u lokal olarak hesaplamalidir.
Onerilen davranis:
displayState == countdownisescheduledStartAtparse edilircreatorNameana katilimci label'i olarak gösterilirstreamTitlebaslik olarak gösterilirstatusTextcountdown ustu veya alti sabit aciklama metni olarak gösterilirsubtitleikincil yardimci metin olarak gösterilir- kalan sure cihaz saatine gore hesaplanır
- countdown UI saniye bazli veya dakika bazli guncellenebilir
viewerCountsu an backend tarafında sabit128değeriyle gönderilir- tiklandiginda tercihen
deepLinkUrl, fallback olarakdeepLinkkullanılır
Basit parse ornegi:
func parseScheduledStart(_ value: Int?) -> Date? {
guard let value else { return nil }
return Date(timeIntervalSince1970: TimeInterval(value))
}5. Countdown bittiginde lokal davranis
UI onerisi:
scheduledStartAtgecildiginde countdown00:00seviyesinde sabitlenebilir- Tiklandiginda
deepLinkile yayin ekranina yonlenilebilir - Istiyorsaniz countdown tamamlandiginda minimal bir "basliyor" veya pasif state gösterilebilir
- Backend
cancelled/ready/started/expiredanlarindadisplayState: endedpayload'i gönderebilir; widget bu state'i de decode etmelidir
Backend Lifecycle Haritasi
Backend tarafında Live Activity akisi su noktalarda tetiklenir:
- Planli yayin olusturulunca:
plannedStartDate - 60 dakikaiçin queue job olusturulur- yayin 60 dakikadan daha yakin ise start hemen dispatch edilir
Ek davranislar:
- Creator ve guest hedeflenir
- Ayni kullanıcı-cihaz-yayin kombinasyonunda duplicate start engellenir
- Sadece
platform = ios,deviceId, FCM token veliveActivityPushToStartTokenolan cihazlar hedeflenir live_streamnotification preference kapaliysa activity baslatilmaz- Kullanıcı push-to-start token'i gec kaydederse, yakin 60 dakika içindeki uygun scheduled stream'ler için backend backfill dener
- Runtime token kaydi geldikten sonra backend stream
cancelled/ready/started/expiredanlarindaendpush gönderebilir - Stream
POST /live-stream/:id/cancelendpoint'i ile iptal edilirse backend ilgili pre-start reminder job'larini, aktif user reminder kayitlarini ve Live Activity end akisina bagli cleanup'i tetikler
Framework Bagimsiz Sequence Diagram
sequenceDiagram
participant App as iOS App
participant AK as ActivityKit
participant API as Backend API
participant Q as Queue
participant FCM as FCM/APNs
App->>AK: pushToStartTokenUpdates dinle
AK-->>App: push-to-start token
App->>API: POST /notifications/device-token
API->>Q: T-60 start job schedule et
Q-->>API: Job tetiklenir
API->>FCM: Live Activity start push
FCM-->>AK: remote start
AK-->>App: Live Activity gorunur
App->>API: POST /notifications/live-activity-sessions
API-->>App: session kaydi tamam
API->>FCM: gerekiyorsa Live Activity end push
FCM-->>AK: remote endEdge Case ve Uyari Notlari
1. Start push'u tamamen sessiz değil
Apple'in ActivityKit push kurallarina gore remote-start payload'inda alert bulunmasi gerekir. Bu nedenle backend start push'unda minimum bir alert icerigi gönderir.
Bu ne anlama gelir:
- Ayrica klasik bir normal push notification kaydi uretilmez
- Ancak Live Activity start payload'inin kendi alert'i vardir
- Alert metni backend tarafında lokalize edilir
2. scheduledStartAt tipi epoch seconds olarak ele alınmali
Backend scheduledStartAt alanini epoch seconds integer olarak gönderir. ContentState içinde bunu Int? alip render asamasinda Date(timeIntervalSince1970:) ile parse etmek daha guvenlidir.
3. deviceId stabil olmali
Farkli deviceId kullanılırsa:
- backend yeni cihaz gibi davranir
- duplicate start korumasi zayiflayabilir
- ayni fiziksel cihaz birden fazla cihaz gibi görünebilir
Bu nedenle cihaz bazli kalici tek bir deviceId seçilip tüm cagrilarda ayni deger kullanılmalıdır.
Pratik oneriler:
UIDevice.current.modelveya cihaz pazarlama adinideviceIdolarak kullanmayin- Keychain veya benzeri kalici bir storage'da saklanan UUID kullanın
- Uygulama acilisinda yeniden generate etmeyin; ayni cihaz için ayni deger kalmalidir
4. Runtime token kaydi zorunludur
Backend'in activity'yi guvenilir sekilde kapatabilmesi için mobil tarafin liveActivityToken bilgisini POST /notifications/live-activity-sessions ile kaydetmesi gerekir. Bu adim atlanirsa backend sadece start yapabilir, end yapamaz.
5. iOS 18 channel akisi bu entegrasyonda yok
Backend su anda ActivityKit channel/broadcast modeli kullanmiyor. Mobil taraf sadece:
push-to-starttoken
akisini implement etmelidir.
Manual QA Checklist
- iOS 17.2+ cihazda login ol.
POST /notifications/device-tokençağrısı FCM token,deviceIdveliveActivityPushToStartTokenile atiliyor mu kontrol et.- 60 dakikadan fazla ileri tarihli bir scheduled stream olustur.
- Zaman yaklastiginda Live Activity remote-start oluyor mu kontrol et.
- Widget countdown'u
scheduledStartAtile dogru hesapliyor mu kontrol et. - Remote-start sonrasi
POST /notifications/live-activity-sessionsçağrısı geliyor mu kontrol et. - Stream cancel/ready/start/expire oldugunda backend end push'u gönderebiliyor mu kontrol et.
- Stream cancel endpoint'i cagrildiginda pre-start reminder job'lari ve aktif user reminder kayitlari temizleniyor mu kontrol et.
- App restart sonrasi
push-to-starttoken tekrar register ediliyor mu kontrol et. - Ayni yayin için duplicate start olusmuyor mu kontrol et.