iOS License Management
Documento tecnico che descrive come vengono gestite le licenze nell'app iOS, inclusi i punti di controllo, upgrade/downgrade, e integrazione con Apple StoreKit 2.
Architetturaβ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β iOS App (StoreKit 2) β
β β
β βββββββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββββββββ β
β β StoreManager β β PlanSelection β β SubscriptionManagementβ β
β β - purchase() β β - device count β β - status display β β
β β - restore() β β - period select β β - upgrade/downgrade β β
β ββββββββββ¬βββββββββ ββββββββββ¬ββββββββββ βββββββββββββ¬ββββββββββββ β
β β β β β
β ββββββββββββββββββββββββΌβββββββββββββββββββββββββββ β
β β β
β βΌ β
β ββββββββββββββββ β
β β ApiClient β β
β β - getLicense β β
β β - getStatus β β
β ββββββββ¬ββββββββ β
ββββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Backend β
β β
β βββββββββββββββββββ βββββββββββββββββββββββββββββββββββββββ β
β β Billing Service ββββββββββββββββ Apple S2S v2 Webhook β β
β β β β POST /api/billing/webhooks/apple β β
β β subscriptions β βββββββββββββββββββββββββββββββββββββββ β
β β - user_id β β
β β - plan_id βββββ plan_id = "annual_3" (3 dispositivi, annuale) β
β β - status β β
β ββββββββββ¬βββββββββ β
β β β
β βΌ β
β βββββββββββββββββββ β
β β Devices Service β β
β β β β
β β - active count βββββ Quanti dispositivi ha l'utente attivi β
β β - suspended βββββ Dispositivi sospesi per limite licenze β
β βββββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Piano di Licenzeβ
Le licenze sono codificate nel plan_id:
| plan_id | Periodo | Dispositivi |
|---|---|---|
monthly_1 | Mensile | 1 |
monthly_2 | Mensile | 2 |
annual_3 | Annuale | 3 |
annual_5 | Annuale | 5 |
Formato: {periodo}_{numero_dispositivi}
Punti di Controllo Licenzeβ
1. Aggiunta Dispositivo (DevicesListView)β
Quando l'utente preme "+" per aggiungere un nuovo dispositivo:
// DevicesListView.swift - checkLicenseAndShowAddDevice()
1. Chiama ApiClient.getLicenseStatus()
2. Controlla:
- Se allowed == 0 β Mostra PlanSelectionView (reason: .noLicense)
- Se active >= allowed β Mostra PlanSelectionView (reason: .upgradeLicense)
- Altrimenti β Mostra AddDeviceView
File: VislaGPS/Views/DevicesListView.swift:205-247
2. Riattivazione Dispositivo (DeviceDetailView)β
Quando l'utente vuole riattivare un dispositivo sospeso:
// DeviceDetailView.swift - reactivateDevice()
1. Chiama ApiClient.getLicenseStatus()
2. Se active >= allowed β Mostra SubscriptionRequiredModal
3. Altrimenti β Procedi con riattivazione
File: VislaGPS/Views/DeviceDetailView.swift
3. Gestione Piano (SubscriptionManagementView)β
Accessibile da Impostazioni β "Gestisci Piano":
// SubscriptionManagementView.swift
Mostra:
- Status abbonamento (Attivo/Scaduto)
- Provider (Apple/Stripe)
- Utilizzo licenze (X/Y attive)
- Pulsante "Modifica Piano" β PlanSelectionView
- Pulsante "Disdici Abbonamento" β App Store
File: VislaGPS/Views/SubscriptionManagementView.swift
Flusso Upgrade/Downgradeβ
Upgrade (es. da 1 a 3 dispositivi)β
βββββββββββββββββββ
β User ha 1 lic. β
β Vuole aggiungereβ
β 2Β° dispositivo β
ββββββββββ¬βββββββββ
β
βΌ
βββββββββββββββββββ
β + tapped β
β getLicenseStatusβ
β active >= allowedβ
ββββββββββ¬βββββββββ
β
βΌ
βββββββββββββββββββ
β PlanSelectionViewβ
β reason: upgrade β
β current: 1 β
β default: 2 β
ββββββββββ¬βββββββββ
β
βΌ
βββββββββββββββββββ
β Pulsante mostra β
β "Aumenta Licenze"β
β (non "Abbonati")β
ββββββββββ¬βββββββββ
β
βΌ
βββββββββββββββββββ βββββββββββββββββββ
β StoreKit 2 ββββββΆβ Apple processa β
β purchase() β β upgrade prorata β
βββββββββββββββββββ ββββββββββ¬βββββββββ
β
βΌ
βββββββββββββββββββ
β Webhook S2S v2 β
β DID_CHANGE_* β
ββββββββββ¬βββββββββ
β
βΌ
βββββββββββββββββββ
β Backend aggiornaβ
β plan_id: "X_3" β
βββββββββββββββββββ
Downgrade (es. da 3 a 1 dispositivo)β
βββββββββββββββββββ
β User ha 3 lic. β
β Vuole ridurre β
β a 1 licenza β
ββββββββββ¬βββββββββ
β
βΌ
βββββββββββββββββββ
β PlanSelectionViewβ
β Seleziona 1 β
β pulsante: β
β "Riduci Licenze"β
ββββββββββ¬βββββββββ
β
βΌ
βββββββββββββββββββ
β Controllo: β
β activeDevices=2 β
β selectedPlan=1 β
β β
β ERRORE! Non puoiβ
β ridurre a 1 con β
β 2 dispositivi β
β attivi β
βββββββββββββββββββ
Messaggio: "Hai 2 dispositivi attivi. Per passare al piano da 1
dispositivo, devi prima sospendere o rimuovere i dispositivi in eccesso."
Protezione Downgrade: L'utente non puΓ² ridurre le licenze a meno di quanti dispositivi attivi ha.
PlanSelectionView - Logica Pulsanteβ
// PlanSelectionView.swift
private var purchaseButtonText: String {
if isChangingPlan {
if selectedDevices == currentDevices {
return "Piano Attuale" // Grigio, disabilitato
} else if selectedDevices > currentDevices {
return "Aumenta Licenze" // Blu, attivo
} else {
return "Riduci Licenze" // Blu, attivo (con controllo)
}
}
return "Abbonati Ora" // Nuovo abbonamento
}
| Stato | currentDevices | selectedDevices | Pulsante |
|---|---|---|---|
| Nuovo utente | nil/0 | 1 | "Abbonati Ora" |
| Ha 1 lic | 1 | 1 | "Piano Attuale" (disabled) |
| Ha 1 lic | 1 | 2 | "Aumenta Licenze" |
| Ha 3 lic | 3 | 1 | "Riduci Licenze" |
| Ha 3 lic | 3 | 5 | "Aumenta Licenze" |
Integrazione Apple StoreKit 2β
appAccountTokenβ
L'app passa l'user_id ad Apple in formato UUID:
// StoreManager.swift
let appAccountToken = UUID(uuidString:
String(format: "%08x-0000-0000-0000-000000000000", userId)
) ?? UUID()
// Esempio: userId=2 β "00000002-0000-0000-0000-000000000000"
Webhook Backendβ
Il billing service estrae l'user_id dall'appAccountToken:
# apple_provider.py - _extract_user_id()
def _extract_user_id(self, app_account_token: str) -> int:
# Parse UUID format: "00000002-0000-0000-0000-000000000000"
parts = app_account_token.split("-")
user_id_hex = parts[0] # "00000002"
return int(user_id_hex, 16) # β 2
Configurazione App Store Connectβ
- Production URL:
https://api.vislagps.com/api/billing/webhooks/apple - Sandbox URL:
https://api-dev.vislagps.com/api/billing/webhooks/apple - Version: 2 (S2S v2)
Tempi di Aggiornamentoβ
Sandbox vs Produzioneβ
| Operazione | Sandbox | Produzione |
|---|---|---|
| Nuovo acquisto | ~1 min webhook | Secondi |
| Upgrade | Attende scadenza periodo (3-5 min) | Immediato + prorata |
| Downgrade | Attende scadenza | Al rinnovo successivo |
| Cancellazione | Attende scadenza | Al rinnovo successivo |
Nota Importante: In sandbox gli upgrade sembrano lenti perchΓ© Apple aspetta la scadenza del periodo (3-5 minuti per mensile). In produzione, gli upgrade sono immediati con calcolo prorata del credito.
API Endpoints Utilizzatiβ
GET /api/devices/license/statusβ
Restituisce lo stato licenze dell'utente.
{
"allowed": 3, // Licenze acquistate
"active": 2, // Dispositivi attivi
"suspended": 0 // Dispositivi sospesi
}
GET /api/billing/subscriptions/check/{user_id}β
Restituisce lo stato abbonamento.
{
"is_subscribed": true,
"has_active_subscription": true,
"status": "ACTIVE",
"provider": "apple",
"plan_id": "annual_3",
"devices": 3,
"expires_at": "2026-01-15T00:00:00Z"
}
File Chiaveβ
| File | ResponsabilitΓ |
|---|---|
StoreManager.swift | Gestione acquisti StoreKit 2 |
PlanSelectionView.swift | UI selezione piano, logica upgrade/downgrade |
SubscriptionManagementView.swift | UI gestione abbonamento esistente |
DevicesListView.swift | Controllo licenze prima di aggiungere dispositivo |
DeviceDetailView.swift | Controllo licenze per riattivazione |
SubscriptionRequiredModal.swift | Modal per acquisto licenze aggiuntive |
ApiClient.swift | Chiamate API per license/subscription status |
Troubleshootingβ
"Acquisto completato ma licenze non aggiornate"β
- In Sandbox: Aspetta 3-5 minuti per il webhook
- Controlla i log del billing service:
docker logs billing --tail 100 - Verifica che il webhook arrivi: cerca "Received Apple webhook"
- Verifica estrazione user_id: cerca "Extracted user_id"
"Non posso ridurre le licenze"β
L'utente deve prima sospendere/rimuovere dispositivi fino ad averne meno del piano target.
"Disdici abbonamento non funziona"β
Il pulsante apre l'App Store. La cancellazione effettiva avviene lΓ . Il backend riceverΓ il webhook quando Apple processa la cancellazione.