Skip to main content

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_idPeriodoDispositivi
monthly_1Mensile1
monthly_2Mensile2
annual_3Annuale3
annual_5Annuale5

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
}
StatocurrentDevicesselectedDevicesPulsante
Nuovo utentenil/01"Abbonati Ora"
Ha 1 lic11"Piano Attuale" (disabled)
Ha 1 lic12"Aumenta Licenze"
Ha 3 lic31"Riduci Licenze"
Ha 3 lic35"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​

OperazioneSandboxProduzione
Nuovo acquisto~1 min webhookSecondi
UpgradeAttende scadenza periodo (3-5 min)Immediato + prorata
DowngradeAttende scadenzaAl rinnovo successivo
CancellazioneAttende scadenzaAl 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​

FileResponsabilitΓ 
StoreManager.swiftGestione acquisti StoreKit 2
PlanSelectionView.swiftUI selezione piano, logica upgrade/downgrade
SubscriptionManagementView.swiftUI gestione abbonamento esistente
DevicesListView.swiftControllo licenze prima di aggiungere dispositivo
DeviceDetailView.swiftControllo licenze per riattivazione
SubscriptionRequiredModal.swiftModal per acquisto licenze aggiuntive
ApiClient.swiftChiamate API per license/subscription status

Troubleshooting​

"Acquisto completato ma licenze non aggiornate"​

  1. In Sandbox: Aspetta 3-5 minuti per il webhook
  2. Controlla i log del billing service: docker logs billing --tail 100
  3. Verifica che il webhook arrivi: cerca "Received Apple webhook"
  4. 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.