Skip to content

fix(ios): safe AnyHashable-to-String cast in notification userInfo handling#11

Closed
sergio-silva-dito wants to merge 1 commit intomainfrom
fix/flutter-ios-anyhasable-string-cast
Closed

fix(ios): safe AnyHashable-to-String cast in notification userInfo handling#11
sergio-silva-dito wants to merge 1 commit intomainfrom
fix/flutter-ios-anyhasable-string-cast

Conversation

@sergio-silva-dito
Copy link
Copy Markdown
Contributor

@sergio-silva-dito sergio-silva-dito commented Mar 6, 2026

Contexto

O plugin iOS recebe payloads de notificação via APNs com o tipo [AnyHashable: Any] — padrão do sistema para dicionários que chegam do Objective-C. As chaves nesses dicionários são tipicamente NSString bridgeadas, não Swift.String nativas.

O problema central é que cast bulk de [AnyHashable: Any] para [String: Any] não é garantido em Swift: ele exige que todas as chaves já sejam Swift.String nativos, sem a canonicalização automática que AnyHashable oferece individualmente. Se qualquer chave não passar nessa verificação, o cast inteiro falha e retorna nil.

Problemas corrigidos

1. emitNotificationClickEvent — inconsistência de tipos no fallback

// antes (main)
let source = (userInfo["data"] as? [String: Any]) ?? userInfo

Se userInfo["data"] não puder ser castado para [String: Any], o fallback era o próprio userInfo tipado como [AnyHashable: Any]. Isso causava uma inconsistência de tipos: source seria [AnyHashable: Any], mas os acessos subsequentes (source["notification"], source["reference"], etc.) dependem de chaves String — sem garantia de funcionamento correto dependendo do runtime.

2. channelFromUserInfo — notificações Dito potencialmente ignoradas

// antes (main)
if let data = userInfo["data"] as? [String: Any], let ch = data["channel"] as? String {

O cast bulk para [String: Any] pode falhar com payloads APNs reais. Se isso acontecer, a função retorna nil, isDitoChannel retorna false e todas as notificações Dito são silenciosamente ignoradas.

Solução

Helper toStringKeyed

Adicionado um helper privado que converte [AnyHashable: Any] para [String: Any] de forma segura, chave a chave:

private static func toStringKeyed(_ dict: [AnyHashable: Any]) -> [String: Any] {
  Dictionary(uniqueKeysWithValues: dict.compactMap { key, value -> (String, Any)? in
    guard let stringKey = key.base as? String else { return nil }
    return (stringKey, value)
  })
}

O pior caso é descartar uma chave individual que genuinamente não seja String — nunca o payload inteiro.

channelFromUserInfo

O cast intermediário passa a usar [AnyHashable: Any], que é sempre seguro para subdicionários vindos do APNs. O acesso subsequente data["channel"] as? String permanece correto porque AnyHashable canonicaliza tipos bridgeados do Foundation individualmente: ao criar um AnyHashable a partir de um NSString, Swift o normaliza internamente para Swift.String antes de calcular hash e comparar igualdade. Assim, o subscript com uma String Swift encontra corretamente uma chave armazenada como NSString, e o cast escalar as? String faz o bridge automaticamente.

// depois
if let data = userInfo["data"] as? [AnyHashable: Any], let ch = data["channel"] as? String {

Essa canonicalização existe apenas no acesso individual via subscript — não no cast bulk de dicionário (as? [String: Any]), que é exatamente o problema que esta correção evita.

emitNotificationClickEvent

Substituído o cast frágil pelo helper toStringKeyed, eliminando a inconsistência de tipos:

// depois
let rawData = (userInfo["data"] as? [AnyHashable: Any]) ?? userInfo
let source = DitoSdkPlugin.toStringKeyed(rawData)

Test plan

  • Receber uma notificação Dito com o app em foreground e verificar que os campos (notificationId, reference, logId, notificationName, userId) chegam corretamente no Flutter
  • Receber uma notificação Dito com o app em background, clicar nela e verificar o mesmo
  • Verificar que notificações de outros canais (não-Dito) continuam sendo ignoradas corretamente

🤖 Generated with Claude Code

…ndling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@sergio-silva-dito
Copy link
Copy Markdown
Contributor Author

Substituído por abordagem mais simples: manter source como [AnyHashable: Any] evitando conversão desnecessária e perda de chaves.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant