Notificações push - iOS
Visão geral
O iOS exige que os aplicativos de VoIP utilizem PushKit para notificações de chamadas recebidas e CallKit para apresentar a interface de usuário da chamada do sistema. Essas duas estruturas estão intimamente interligadas — a Apple exige que cada notificação de VoIP resulte em uma reportNewIncomingCall ao CallKit. O não cumprimento dessa exigência faz com que o iOS revogue permanentemente o envio de notificações push para o dispositivo.
O Vonage Client SDK se integra a ambas as estruturas. Seu aplicativo é responsável por:
- Cadastro para notificações por VoIP e encaminhamento do token para a Vonage
- Receber cargas úteis de notificações push e encaminhá-las ao SDK para processamento
- Apresentação de chamadas recebidas por meio do CallKit
- Gerenciando o ciclo de vida da sessão de áudio exclusivamente por meio de callbacks do CallKit
- Manter uma sessão SDK válida para que as chamadas possam ser atendidas
Responsabilidades dos componentes
| Componente | Responsabilidade |
|---|---|
| PushKit | Reativa o aplicativo do estado encerrado/suspenso e entrega a mensagem push |
| SDK da Vonage | Decodifica a carga útil do push, gerencia a sessão WebRTC e lida com a sinalização da chamada |
| CallKit | Apresenta a interface do usuário para chamadas de sistema e gerencia a ativação de sessões de áudio |
| Seu aplicativo | Coordena os processos acima e gerencia o ciclo de vida do JWT e da sessão |
Configuração do projeto
1 - Recursos do Xcode
Habilite o seguinte no arquivo de destino Assinatura e recursos guia:
Push Notifications → enabled
Background Modes → Voice over IP (checked)
→ Background processing (checked)
Seu arquivo de direitos deve conter:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.pushkit.unrestricted-voip</key>
<true/>
</dict>
</plist>
Aviso: O aps-environment O valor é definido automaticamente pelo Xcode com base no arquivo de provisionamento. Não o altere manualmente.
Direito restrito ao VoIP
A partir do iOS 16, os aplicativos sem o com.apple.developer.pushkit.unrestricted-voip A cota está limitada a um pequeno número de notificações do PushKit por dia. Se o limite do sistema for excedido, o envio de notificações será reduzido de forma silenciosa e as chamadas recebidas não ativarão o aplicativo.
Esse direito deve ser solicitado à Apple por meio do formulário de solicitação de direito. Até que seja concedido, as compilações de desenvolvimento podem funcionar normalmente (as compilações de depuração têm um limite maior), mas as compilações de produção terão sua taxa de processamento limitada.
Certifique-se de que essa autorização esteja presente no seu .entitlements enviado e aprovado pela Apple antes de ser publicado na App Store.
2 - Certificados de notificação push
Você precisa de um certificado APNs para o seu App ID, exportado como um .p12, e carregado no aplicativo da Vonage com o qual seu aplicativo realiza a autenticação. Um certificado SSL do APNs moderno, restrito ao escopo do aplicativo, é válido por .voip assunto; portanto, um “Certificado de Serviços de VoIP” legado separado é não obrigatório.
Gerar um CSR
Em Acesso a Chaves → Assistente de Certificados → Solicitar um certificado a uma autoridade certificadora, digite seu e-mail e um nome comum, deixe o campo “E-mail da CA” em branco, escolha Salvo no disco, e salve o .certSigningRequest.
Criar o certificado
- No Portal do Desenvolvedor da Apple → Certificados, Identificadores e Perfis, certifique-se de que seu ID do aplicativo tem o Notificações push recurso ativado.
- Sob Certificados, adicione um Serviço de Notificações Push da Apple via SSL (Sandbox e Produção) certificado, selecione seu App ID e envie o CSR.
- Baixe o arquivo resultante
.cer.
Exportar o .p12
Clique duas vezes no .cer para importá-la para o seu chaveiro de login. Em Meus certificados, encontrar Apple Push Services: <your-bundle-id> — ela deve ter a chave privada aninhada nela — então clique com o botão direito do mouse → Exportar → Troca de Informações Pessoais (.p12). Deixe a senha de exportação em branco, se possível (veja a observação sobre o envio abaixo).
Fazer upload pelo Painel de Controle da Vonage
- Faça login no Painel do Vonage.
- Acesse Applications → seu aplicativo (o
application_id(no qual o JWT do seu aplicativo é autenticado). - Abra o Ativar notificações push aba e faça o upload do
.p12. - O campo de senha é opcional e está identificado como não recomendado (A Vonage armazena esse certificado junto com sua chave privada), por isso é preferível usar um certificado sem senha.
Veja o Documentação de configuração de notificações push do Vonage para iOS para referência.
Os certificados expiram anualmente. O modo de falha é completo e silencioso — nenhum erro é detectado em nenhuma parte do sistema. Acompanhe a data de validade e renove de forma proativa.
Aviso: Sandbox x Produção
As compilações de depuração do Xcode utilizam o ambiente APNs da sandbox; as compilações do TestFlight e da App Store utilizam o ambiente de produção. Enviar certificados separados para cada ambiente e sempre determinar o isSandbox sinalizador em tempo de compilação usando #if DEBUG — nunca o defina diretamente no código.
3 - Estruturas
Adicionar CallKit abaixo de Frameworks, bibliotecas e conteúdo incorporado na guia “Geral” do seu destino. Importe conforme necessário:
import PushKit
import CallKit
import VonageClientSDKVoice
import AVFoundation
4 - Inicialização do SDK
Inicializar VGVoiceClient uma vez, no início do ciclo de vida do aplicativo (por exemplo, no seu AppDelegate ou em um gerenciador singleton). Defina seu delegado antes de se registrar para notificações push ou criar sessões:
let config = VGClientInitConfig(loggingLevel: .info)
#if targetEnvironment(simulator)
config.enableWebsocketInvites = true
#endif
let client = VGVoiceClient(config)
client.delegate = self
Solicite permissão para usar o microfone logo no início do ciclo de vida do aplicativo — de preferência em application(_:didFinishLaunchingWithOptions:):
AVAudioSession.sharedInstance().requestRecordPermission { granted in
print("Microphone permission: \(granted)")
}
Se o usuário negar o acesso ao microfone, as chamadas serão estabelecidas, mas a pessoa do outro lado da linha ouvirá apenas silêncio.
Integração com o PushKit
Inscreva-se para receber notificações de VoIP
Criar um PKPushRegistry e definir os tipos de notificações desejados na inicialização do aplicativo, antes que qualquer notificação seja recebida.
#if DEBUG
let isSandbox = true
#else
let isSandbox = false
#endif
client.registerVoipToken(pushToken, withDeviceToken: nil, isSandbox: isSandbox) { error, deviceId in
if let error = error {
print("Failed to register push token: \(error.localizedDescription)")
return
}
// Store deviceId for unregistration on logout
print("Push token registered, deviceId: \(deviceId ?? "")")
}
PKPushRegistry *pushRegistry = [[PKPushRegistry alloc] initWithQueue:dispatch_get_main_queue()];
pushRegistry.delegate = self;
pushRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP];
PKPushRegistryDelegate
Implemente os três métodos delegados. O terceiro — didReceiveIncomingPushWith — é o mais importante:
extension AppDelegate: PKPushRegistryDelegate {
// Called when a new VoIP token is issued or rotated
func pushRegistry(_ registry: PKPushRegistry,
didUpdate pushCredentials: PKPushCredentials,
for type: PKPushType) {
guard type == .voIP else { return }
CallManager.shared.voipTokenDidUpdate(pushCredentials.token)
}
// Called when the token is invalidated (e.g. app reinstall)
func pushRegistry(_ registry: PKPushRegistry,
didInvalidatePushTokenFor type: PKPushType) {
CallManager.shared.unregisterDeviceToken()
}
// Called when an inbound VoIP push is received
func pushRegistry(_ registry: PKPushRegistry,
didReceiveIncomingPushWith payload: PKPushPayload,
for type: PKPushType,
completion: @escaping () -> Void) {
guard type == .voIP else { completion(); return }
CallManager.shared.handleIncomingPush(payload: payload, completion: completion)
}
}
Registrando o token na Vonage
Cadastre-se no client.registerVoipToken(_:isSandbox:) somente quando houver uma sessão da Vonage. Armazene o valor retornado deviceId — você precisa disso para cancelar o registro ao sair da conta.
import PushKit
let pushRegistry = PKPushRegistry(queue: .main)
pushRegistry.delegate = self
pushRegistry.desiredPushTypes = [.voIP]
#if DEBUG
BOOL isSandbox = YES;
#else
BOOL isSandbox = NO;
#endif
[client registerVoipToken:pushToken withDeviceToken:nil isSandbox:isSandbox callback:^(NSError * _Nullable error, NSString * _Nullable deviceId) {
if (error != nil) {
NSLog(@"Failed to register push token: %@", error);
return;
}
// Store deviceId for unregistration on logout
NSLog(@"Push token registered, deviceId: %@", deviceId);
}];
Não ligue registerVoipToken a cada restauração da sessão. Uma vez registrado, o token permanece válido até que seja renovado ou até que o usuário saia da conta. O registo deve ser repetido a cada createSession acumula entradas de dispositivos e pode resultar em envios duplicados.
Gerenciamento de sessões
Uma sessão do Vonage (client.createSession(jwt)) é necessário para que o SDK possa processar convites de chamada ou executar ações relacionadas a chamadas. Quando chega uma notificação de VoIP e o aplicativo é encerrado ou a sessão expira, a sessão deve ser recriada. A rapidez com que isso ocorre determina diretamente se os usuários conseguirão atender as chamadas com sucesso.
A falha de produção mais comum
Não faça o seguinte:
Ao receber uma notificação push: chame seu servidor de autenticação -> obtenha um JWT atualizado -> chame createSession. Isso adiciona dois saltos de rede serial em uma janela de execução em segundo plano com sinal fraco. O usuário vê a tela do CallKit, toca em “Atender” antes que a sessão esteja pronta e a chamada falha sem aviso prévio.
O que fazer:
No momento do login do usuário: armazenar o JWT (e um token de atualização) localmente. Ao receber uma notificação push: chamar createSession diretamente com o JWT armazenado. Zero chamadas ao backend no caminho rápido.
Estratégia da JWT
Siga uma cadeia de prioridades — use o caminho mais rápido disponível:
| Prioridade | Condição | Ação | Latência típica |
|---|---|---|---|
| 1º | Existe uma sessão ativa | Devolva imediatamente — não é necessário fazer nada | 0 ms |
| 2º | O JWT armazenado é válido | client.createSession(storedJWT) |
100-400 ms |
| 3º | O JWT expirou, mas há um token de atualização disponível | Atualizar endpoint -> novo JWT -> createSession |
300–1.000 ms |
| 4º | Sem credenciais | Aviso no log; não é possível atender a chamada | 0 ms |
Criação de sessão
do {
let sessionId = try await client.createSession(jwt)
print("Session created: \(sessionId)")
} catch {
print("Failed to create session: \(error.localizedDescription)")
}
[client createSession:jwt callback:^(NSError * _Nullable error, NSString * _Nullable sessionId) {
if (error != nil || sessionId == nil) {
NSLog(@"Failed to create session: %@", error);
return;
}
NSLog(@"Session created: %@", sessionId);
}];
Restauração da sessão ao receber uma notificação push
func restoreSessionIfNeeded() {
guard activeSessionId == nil else {
print("Session already active")
return
}
if let jwt = UserDefaults.standard.string(forKey: "vonage_jwt") {
// Fast path — use stored JWT directly
createSession(jwt: jwt)
} else if let refreshToken = storedRefreshToken {
// Slow path — exchange refresh token for a new JWT first
refreshJWT(refreshToken) { [weak self] newJWT in
self?.createSession(jwt: newJWT)
}
} else {
print("No credentials available for session restoration")
}
}
Processamento de cargas úteis de push
Essa é a parte da integração que mais depende do momento certo. Duas operações devem ocorrer a partir de handleIncomingPush, e a relação de ordem entre elas não é óbvia.
O Padrão Concorrente (Obrigatório)
Nunca aguarde a restauração da sessão antes de chamar processCallInvitePushData. Retardar o processamento por push por meio de gating createSession atrasos reportNewIncomingCall por uma viagem de ida e volta completa pela rede. A Apple considera isso uma falha no processamento da notificação push — o PushKit interrompe silenciosamente o envio de futuras notificações push para o dispositivo.
Implementação
client.processCallInvitePushData(payload.dictionaryPayload)
[client processCallInvitePushData:payload.dictionaryPayload];
O padrão de implementação completo:
private var pendingPushCompletion: (() -> Void)?
func handleIncomingPush(payload: PKPushPayload, completion: @escaping () -> Void) {
// 1. Validate: only handle Vonage incoming-call pushes
let pushType = VGVoiceClient.vonagePushType(payload.dictionaryPayload)
guard pushType == .incomingCall else {
// Must still report to CallKit — failing to do so breaks future push delivery.
let fakeUUID = UUID()
callProvider.reportNewIncomingCall(with: fakeUUID, update: CXCallUpdate()) { _ in
self.callProvider.reportCall(with: fakeUUID, endedAt: .now, reason: .failed)
completion()
}
return
}
// 2. Store completion — will be called after reportNewIncomingCall
pendingPushCompletion = completion
// 3a. Restore session concurrently — non-blocking
restoreSessionIfNeeded()
// 3b. Process the push immediately — triggers didReceiveInviteForCall
client.processCallInvitePushData(payload.dictionaryPayload)
}
Aviso: Mesmo que a notificação esteja com formato incorreto, tenha expirado ou seja uma duplicata, você deve ainda ligar reportNewIncomingCall e encerrar a chamada imediatamente. Ignorar essa etapa interrompe a entrega push de forma permanente.
Integração com o CallKit
Configuração do CXProvider
Criar CXProvider e CXCallController Crie-os uma única vez e mantenha-os durante toda a vida útil do seu gerenciador. Não os recrie a cada chamada ou envio.
class CallManager: NSObject {
static let shared = CallManager()
private let callProvider: CXProvider
private let callController = CXCallController()
private override init() {
let config = CXProviderConfiguration()
config.supportsVideo = false
config.maximumCallGroups = 1
config.maximumCallsPerCallGroup = 1
config.supportedHandleTypes = [.generic]
config.supportsDTMF = true
config.supportsHolding = true
callProvider = CXProvider(configuration: config)
super.init()
callProvider.setDelegate(self, queue: nil)
}
}
Como relatar uma chamada recebida
Chamado de didReceiveInviteForCall (consulte a seção “Delegado do SDK”). A função de conclusão do PushKit deve ser chamada dentro do reportNewIncomingCall callback — nunca antes disso.
let update = CXCallUpdate()
let handleType: CXHandle.HandleType = channelType == .phone ? .phoneNumber : .generic
update.remoteHandle = CXHandle(type: handleType, value: caller)
update.hasVideo = false
provider.reportNewIncomingCall(with: callId, update: update) { error in
if let error = error {
print("Failed to report incoming call: \(error.localizedDescription)")
}
// Always call the PushKit completion inside this callback
pushCompletion()
}
CXCallUpdate *update = [[CXCallUpdate alloc] init];
CXHandleType handleType = (channelType == VGVoiceChannelTypePhone) ? CXHandleTypePhoneNumber : CXHandleTypeGeneric;
update.remoteHandle = [[CXHandle alloc] initWithType:handleType value:caller];
update.hasVideo = NO;
[provider reportNewIncomingCallWithUUID:callId update:update completion:^(NSError * _Nullable error) {
if (error != nil) {
NSLog(@"Failed to report incoming call: %@", error);
}
// Always call the PushKit completion inside this callback
pushCompletion();
}];
Tipo CXHandle
Uso .phoneNumber para o tipo CXHandle quando VGVoiceChannelType é .phone (Chamadas da rede PSTN com números E.164). Use .generic para chamadas entre aplicativos. Isso afeta a forma como o iOS exibe o autor da chamada na interface de usuário nativa de chamadas e na lista de chamadas recentes.
CXProviderDelegate - Ações do usuário
extension CallManager: CXProviderDelegate {
// Called when CallKit resets (e.g. during system errors or when the
// provider is invalidated). Clean up all active call state.
func providerDidReset(_ provider: CXProvider) {
activeCall = nil
}
// User tapped Answer
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
guard let callId = activeCallId else { action.fail(); return }
client.answer(callId) { error in
if error == nil {
action.fulfill()
} else {
action.fail()
}
}
}
// User tapped End (covers both Reject while ringing and Hang up during call)
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
guard let callId = activeCallId else { action.fail(); return }
let complete: (Error?) -> Void = { error in
error == nil ? action.fulfill() : action.fail()
}
if isCallRinging {
client.reject(callId, callback: complete)
} else {
client.hangup(callId, callback: complete)
}
}
// Mute toggle
func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
guard let callId = activeCallId else { action.fail(); return }
if action.isMuted {
client.mute(callId) { _ in action.fulfill() }
} else {
client.unmute(callId) { _ in action.fulfill() }
}
}
// Hold toggle
func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) {
guard let callId = activeCallId else { action.fail(); return }
if action.isOnHold {
client.enableEarmuff(callId) { _ in action.fulfill() }
} else {
client.disableEarmuff(callId) { _ in action.fulfill() }
}
}
// DTMF
func provider(_ provider: CXProvider, perform action: CXPlayDTMFCallAction) {
guard let callId = activeCallId else { action.fail(); return }
client.sendDTMF(callId, withDigits: action.digits) { _ in action.fulfill() }
}
}
Transferência de sessão de áudio
// In CXProviderDelegate - provider(_:didActivate:)
func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
VGVoiceClient.enableAudio(audioSession)
}
// In CXProviderDelegate - provider(_:didDeactivate:)
func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
VGVoiceClient.disableAudio(audioSession)
}
// In CXProviderDelegate - provider:didActivateAudioSession:
[VGVoiceClient enableAudio:audioSession];
// In CXProviderDelegate - provider:didDeactivateAudioSession:
[VGVoiceClient disableAudio:audioSession];
Sempre ativar e desativar o áudio do Vonage exclusivamente dentro de didActivate / didDeactivate. Ativar o áudio antes que o CallKit ative a sessão, ou a partir de um manipulador de botão da interface do usuário, causa erros de roteamento de áudio com o Bluetooth, os AirPods e o CarPlay.
Delegado do SDK
VGVoiceClientDelegate - Eventos de chamadas recebidas
extension CallManager: VGVoiceClientDelegate {
func voiceClient(_ client: VGVoiceClient,
didReceiveInviteForCall callId: VGCallId,
from caller: String,
with type: VGVoiceChannelType) {
guard let callUUID = UUID(uuidString: callId) else { return }
activeCallId = callId
activeCallUUID = callUUID
isCallRinging = true
// Report to CallKit, then call the PushKit completion in the callback
reportIncomingCall(callUUID: callUUID, caller: caller, type: type,
pushCompletion: pendingPushCompletion ?? {})
pendingPushCompletion = nil
}
// Remote party hung up or the call timed out
func voiceClient(_ client: VGVoiceClient,
didReceiveHangupForCall callId: VGCallId,
withQuality callQuality: VGRTCQuality,
reason: VGHangupReason) {
guard let uuid = activeCallUUID else { return }
let cxReason: CXCallEndedReason = switch reason {
case .remoteReject: .declinedElsewhere
case .remoteHangup, .localHangup: .remoteEnded
case .remoteNoAnswerTimeout: .unanswered
default: .failed
}
callProvider.reportCall(with: uuid, endedAt: .now, reason: cxReason)
cleanUpCallState()
}
// Caller cancelled the invite before it was answered
func voiceClient(_ client: VGVoiceClient,
didReceiveInviteCancelForCall callId: VGCallId,
with reason: VGVoiceInviteCancelReason) {
guard let uuid = activeCallUUID else { return }
let cxReason: CXCallEndedReason = switch reason {
case .answeredElsewhere: .answeredElsewhere
case .rejectedElsewhere: .declinedElsewhere
case .remoteCancel: .remoteEnded
case .remoteTimeout: .unanswered
default: .failed
}
callProvider.reportCall(with: uuid, endedAt: .now, reason: cxReason)
cleanUpCallState()
}
private func cleanUpCallState() {
activeCallId = nil
activeCallUUID = nil
isCallRinging = false
}
}
Sempre ligue callProvider.reportCall(with:endedAt:reason:) para todos os caminho de encerramento — desligamento remoto, cancelamento remoto, tempo limite de mídia, rejeição local. A ausência de qualquer um desses itens deixa o CallKit em um estado inconsistente e pode prejudicar o registro de chamadas e os relatórios futuros de chamadas.
Chamadas efetuadas
Para chamadas efetuadas, o fluxo é invertido: seu aplicativo inicia a chamada por meio do SDK e, em seguida, utiliza CXCallController para informar o CallKit.
// 1. Initiate the call via the SDK
func startCall(to callee: String) {
let callContext = ["callee": callee]
client.serverCall(callContext) { [weak self] error, callId in
guard let callId,
let callUUID = UUID(uuidString: callId) else { return }
self?.activeCallId = callId
self?.activeCallUUID = callUUID
// 2. Notify CallKit so the in-call UI appears
let handle = CXHandle(type: .generic, value: callee)
let action = CXStartCallAction(call: callUUID, handle: handle)
let transaction = CXTransaction(action: action)
self?.callController.request(transaction) { [weak self] error in
guard error == nil else { return }
self?.callProvider.reportOutgoingCall(with: callUUID, startedConnectingAt: .now)
}
}
}
// 3. CXStartCallAction — CallKit confirming the transaction
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
action.fulfill()
}
// 4. When the remote side answers
func voiceClient(_ client: VGVoiceClient,
didReceiveLegStatusUpdateForCall callId: VGCallId,
withLegId legId: String,
andStatus status: VGLegStatus) {
guard status == .answered, let uuid = activeCallUUID else { return }
callProvider.reportOutgoingCall(with: uuid, connectedAt: .now)
}
Tratamento de erros
O SDK é acionado didReceiveSessionErrorWith(reason:) quando a conexão WebSocket subjacente for interrompida durante uma sessão ativa. Sempre tente restaurar a sessão automaticamente.
func client(_ client: VGBaseClient,
didReceiveSessionErrorWith reason: VGSessionErrorReason) {
print("Session error: \(reason)")
switch reason {
case .tokenExpired:
// The stored JWT is now invalid — skip it, use refresh token
refreshAndRestoreSession()
case .pingTimeout, .transportClosed:
// Transient network issue — try the stored JWT first
restoreSessionIfNeeded()
default:
restoreSessionIfNeeded()
}
}
private func refreshAndRestoreSession() {
guard let refreshToken = storedRefreshToken else {
clearSession()
return
}
refreshJWT(refreshToken) { [weak self] result in
switch result {
case .success(let newJWT):
self?.createSession(jwt: newJWT)
case .failure:
self?.clearSession()
}
}
}
Limitações do simulador
O PushKit e o CallKit não estão disponíveis no Simulador do iOS. O SDK oferece um modo de convite via WebSocket para desenvolvimento:
let config = VGClientInitConfig(loggingLevel: .info)
#if targetEnvironment(simulator)
config.enableWebsocketInvites = true // receive invites over WebSocket — no push needed
#endif
let client = VGVoiceClient(config)
func answerCall(callId: String) {
#if targetEnvironment(simulator)
client.answer(callId) { error in
print("Answer: \(error?.localizedDescription ?? "OK")")
}
#else
guard let uuid = activeCallUUID else { return }
let action = CXAnswerCallAction(call: uuid)
callController.request(CXTransaction(action: action)) { _ in }
#endif
}
Aviso: Sempre teste todo o fluxo de operações em um dispositivo físico antes do lançamento. O simulador não consegue testar o despertar do aplicativo encerrado, a restauração da sessão sob um limite real de recursos em segundo plano nem a interface do CallKit na tela de bloqueio.
Armadilhas e antipadrões
1 - Chamada da função de conclusão do PushKit antes de reportNewIncomingCall
Não faça o seguinte:
func pushRegistry(_ registry: PKPushRegistry,
didReceiveIncomingPushWith payload: PKPushPayload,
for type: PKPushType,
completion: @escaping () -> Void) {
client.processCallInvitePushData(payload.dictionaryPayload)
completion() // WRONG — called before reportNewIncomingCall
}
Salve a função de conclusão e execute-a exclusivamente dentro de reportNewIncomingCallo callback assíncrono de 's.
2 - Pular corda reportNewIncomingCall para envios inválidos
Não faça o seguinte:
guard pushType == .incomingCall else {
completion() // WRONG — no call reported to CallKit
return
}
O que fazer: Relatar uma chamada fictícia e encerrá-la imediatamente; em seguida, ligar para o número de confirmação:
guard pushType == .incomingCall else {
let fakeUUID = UUID()
callProvider.reportNewIncomingCall(with: fakeUUID, update: CXCallUpdate()) { _ in
self.callProvider.reportCall(with: fakeUUID, endedAt: .now, reason: .failed)
completion()
}
return
}
3 - Aguardando a restauração da sessão antes de processar o envio
Não faça o seguinte:
restoreSessionIfNeeded { // Waits for network...
self.client.processCallInvitePushData(payload) // Too late
}
O que fazer: Disparar os dois ao mesmo tempo:
restoreSessionIfNeeded() // Non-blocking
client.processCallInvitePushData(payload.dictionaryPayload) // Immediate
4 - Obter um JWT do seu backend ao receber uma notificação push
Não faça o seguinte:
func handleIncomingPush(payload: PKPushPayload, completion: @escaping () -> Void) {
fetchJWTFromServer { jwt in // 500ms - 3s network call
self.client.createSession(jwt) // Another network call
self.client.processCallInvitePushData(payload.dictionaryPayload)
}
}
O que fazer: Armazenar o JWT no momento do login. Usá-lo diretamente ao receber notificações push, sem nenhuma chamada ao back-end.
5 - Codificação estática isSandbox: true
Não faça o seguinte:
client.registerVoipToken(token, isSandbox: true) // Breaks TestFlight
O que fazer:
#if DEBUG
let isSandbox = true
#else
let isSandbox = false
#endif
client.registerVoipToken(token, isSandbox: isSandbox)
6 - Re-registro do token de push a cada restauração de sessão
Não faça o seguinte:
func createSession(jwt: String) {
client.createSession(jwt) { _, _ in
self.registerVoipToken(self.storedToken) // Accumulates device entries
}
}
O que fazer: Registrar apenas no login do usuário ou quando didUpdate pushCredentials disparos (rotação de fichas).
7 - Esquecer de cancelar o registro ao sair da conta
Se você pular unregisterDeviceTokens(byDeviceId:) Ao sair da conta, o dispositivo continua recebendo notificações VoIP para esse usuário por tempo indeterminado — inclusive depois que outro usuário fizer login.
8 - Não mapeamento de todos os motivos de desligamento para o CallKit
Todo caminho de terminação de chamada deve passar por callProvider.reportCall(with:endedAt:reason:) com o correto CXCallEndedReason. A falta de um caminho corrompe o log de chamadas do sistema.
9 - Ativação do áudio fora dos callbacks do CallKit
Não faça o seguinte:
func answerButtonTapped() {
VGVoiceClient.enableAudio(AVAudioSession.sharedInstance()) // WRONG
client.answer(callId) { _ in }
}
O que fazer: Tratar o áudio exclusivamente no delegado do CallKit:
func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
VGVoiceClient.enableAudio(audioSession)
}
func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
VGVoiceClient.disableAudio(audioSession)
}
10 - Certificados de push vencidos
Os certificados de VoIP expiram após um ano. A falha ocorre de forma total e silenciosa. Renove-os de forma proativa e Verify a entrega por notificação push imediatamente após o envio de um novo certificado.
Lista de verificação da produção
Utilize esta lista de verificação antes de enviar a mercadoria:
- [ ] Recursos do Xcode configurados — Notificações push ativadas; Modos em segundo plano: Voz sobre IP + Processamento em segundo plano marcados
- [ ] Certificado de envio de VoIP carregado para ambos os ambientes — Sandbox para depuração, Produção para o TestFlight/App Store
- [ ]
isSandboxusos do sinalizador#if DEBUG— nunca definido de forma estática - [ ] O JWT e o token de atualização são armazenados localmente no momento do login — a restauração da sessão não deve exigir uma chamada ao backend
- [ ]
reportNewIncomingCallsolicitado para todas as chamadas VoIP, sem exceção — incluindo notificações inválidas, duplicadas ou que não sejam da Vonage - [ ] A conclusão do PushKit é chamada apenas dentro de
reportNewIncomingCalla função de retorno de chamada — nunca antes, nunca deixei de fazer - [ ] A restauração de sessão e o processamento push ocorrem simultaneamente —
restoreSessionIfNeeded()eprocessCallInvitePushData()disparar de forma independente - [ ] Registro do token somente no login do usuário ou na rotação do token — não em todos os
createSession - [ ]
unregisterDeviceTokenschamado ao efetuar o logout explícito - [ ] Todas as rotas de terminação de chamadas são reportadas ao CallKit — desligamento remoto, cancelamento, tempo limite, rejeição local, falha na transmissão de mídia
- [ ] Áudio ativado/desativado apenas em
didActivate/didDeactivate - [ ] Código do CallKit protegido por
#if !targetEnvironment(simulator)—enableWebsocketInvites = truepara simulador - [ ] Testado em um dispositivo físico com o aplicativo totalmente encerrado — exercícios sobre a condição de corrida de liberação e restauração de sessão do PushKit
- [ ] Testado em um dispositivo físico com o aplicativo em segundo plano (suspenso)
- [ ] Acompanhamento da data de renovação do certificado de push — os certificados expiram após 1 ano