From 4b9c995ed560e43116f82b54be246cc3059ac5d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Scheffer?= Date: Tue, 13 Oct 2026 14:21:40 -0300 Subject: [PATCH 1/2] Show a verified business's name instead of its phone number A verified WhatsApp Business account (a bank, a payment app, a utility) sets no personal pushname, so _lift_contact_identity() found name, shortName and pushname all empty and the chat list fell back to the bare phone number. WhatsApp's own client shows the business's verified name, which WPPConnect serialises as contact.verifiedName; it is now the last fallback for chat["name"], after the address-book name and shortName, and never overwrites a name the chat already has. A saved contact name still wins, through _resolve_contact_name(). Tests pin the lift and the real resolver together, including the existing profile-name (pushname) path for numbers not in the address book. Ported from the TeleZapp fork (reffehcsoibaf/winzapp), where it was confirmed live on accounts such as "99 Pay" and "Verisure Brasil". Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014LQWLrjS1o6y251rYH95wC --- client/changelog_en-US.txt | 2 + client/changelog_es-ES.txt | 2 + client/changelog_pl.txt | 2 + client/changelog_pt-BR.txt | 2 + client/changelog_pt-PT.txt | 2 + client/changelog_ro.txt | 2 + client/changelog_tr-TR.txt | 2 + client/main_window/chats_store.py | 15 ++++++- tests/test_chat_identity.py | 75 +++++++++++++++++++++++++++++++ 9 files changed, 103 insertions(+), 1 deletion(-) diff --git a/client/changelog_en-US.txt b/client/changelog_en-US.txt index 6eb7c38c5..d3c36c90e 100644 --- a/client/changelog_en-US.txt +++ b/client/changelog_en-US.txt @@ -140,6 +140,8 @@ FIXES - Forward dialog: pressing Down arrow in the search field now moves to the first result and focuses the list, as in the conversations search. +- Verified WhatsApp Business accounts (banks, payment apps, utilities) that you have not saved as contacts showed only their phone number in the chat list. They now show the business's verified name, as in WhatsApp. A name saved in your contacts still takes precedence. + V2.0.0.0 The big news in this release is calls: you can now place, answer and reject voice and video calls from WinZapp itself, without picking up your phone, with an accessible call window, keyboard shortcuts, dedicated audio settings and your call history in the new Calls tab. Locked chats, search in archived chats, Turkish and Romanian and several usability improvements arrived too, along with fixes for situations where WinZapp asked you to pair again when it did not need to. Under the hood, the program's code went through a major reorganization: it changes nothing in how you use WinZapp, but it makes it easier to maintain and to fix from here on. diff --git a/client/changelog_es-ES.txt b/client/changelog_es-ES.txt index 07a3a3389..63259d4dd 100644 --- a/client/changelog_es-ES.txt +++ b/client/changelog_es-ES.txt @@ -140,6 +140,8 @@ CORRECCIONES - Diálogo de reenviar: la flecha abajo en el campo de búsqueda ahora va al primer resultado y enfoca la lista, como en la búsqueda de conversaciones. +- Las cuentas de empresa verificadas de WhatsApp (bancos, aplicaciones de pago, compañías de servicios) que no tienes guardadas en contactos aparecían en la lista de chats solo con el número de teléfono. Ahora aparecen con el nombre verificado de la empresa, como en WhatsApp. Un nombre guardado en tus contactos sigue teniendo prioridad. + V2.0.0.0 La gran novedad de esta versión son las llamadas: ahora se pueden hacer, contestar y rechazar llamadas de voz y videollamadas desde el propio WinZapp, sin coger el móvil, con una ventana de llamada accesible, atajos de teclado, configuración de audio propia y el historial en la nueva pestaña Llamadas. También llegan los chats bloqueados, la búsqueda en las conversaciones archivadas, los idiomas turco y rumano y varias mejoras de uso, además de correcciones en situaciones en las que WinZapp pedía vincular de nuevo sin necesidad. Por dentro, el código del programa ha pasado por una gran reorganización: no cambia nada en el uso, pero hace que WinZapp sea más fácil de mantener y de corregir a partir de ahora. diff --git a/client/changelog_pl.txt b/client/changelog_pl.txt index bccbc79f1..69be0de86 100644 --- a/client/changelog_pl.txt +++ b/client/changelog_pl.txt @@ -140,6 +140,8 @@ POPRAWKI - Okno przekazywania: strzałka w dół w polu wyszukiwania przechodzi teraz do pierwszego wyniku i ustawia fokus na liście, tak jak w wyszukiwaniu rozmów. +- Zweryfikowane konta firmowe WhatsApp (banki, aplikacje płatnicze, dostawcy usług), których nie masz zapisanych w kontaktach, były widoczne na liście czatów tylko z numerem telefonu. Teraz pokazują zweryfikowaną nazwę firmy, tak jak w WhatsApp. Nazwa zapisana w kontaktach nadal ma pierwszeństwo. + V2.0.0.0 Najważniejszą nowością tej wersji są połączenia: połączenia głosowe i wideo można teraz nawiązywać, odbierać i odrzucać bezpośrednio w WinZapp, bez sięgania po telefon — z dostępnym oknem połączenia, skrótami klawiszowymi, własnymi ustawieniami dźwięku i historią w nowej karcie Połączenia. Doszły też zabezpieczone czaty, wyszukiwanie w zarchiwizowanych czatach, języki turecki i rumuński oraz kilka usprawnień w obsłudze, a także poprawki sytuacji, w których WinZapp niepotrzebnie prosił o ponowne parowanie. Pod spodem kod programu przeszedł dużą reorganizację: niczego nie zmienia w codziennym używaniu, ale sprawia, że WinZapp będzie łatwiej utrzymywać i naprawiać. diff --git a/client/changelog_pt-BR.txt b/client/changelog_pt-BR.txt index 7e0a22168..0d415125d 100644 --- a/client/changelog_pt-BR.txt +++ b/client/changelog_pt-BR.txt @@ -140,6 +140,8 @@ CORREÇÕES - Diálogo de encaminhar: a seta para baixo no campo de pesquisa agora vai para o primeiro resultado e foca a lista, como na pesquisa de conversas. +- Contas comerciais verificadas do WhatsApp (bancos, aplicativos de pagamento, concessionárias) que você não salvou nos contatos apareciam na lista de conversas só com o número de telefone. Agora aparecem com o nome verificado da empresa, como no WhatsApp. Um nome salvo nos seus contatos continua tendo preferência. + V2.0.0.0 A grande novidade desta versão são as ligações: agora dá para ligar, atender e recusar ligações de voz e de vídeo pelo próprio WinZapp, sem pegar o celular, com uma janela de ligação acessível, atalhos de teclado, configurações de áudio próprias e o histórico na nova aba Ligações. Também chegaram as conversas trancadas, a pesquisa nas conversas arquivadas, os idiomas turco e romeno e várias melhorias de uso, além de correções em situações em que o WinZapp pedia para parear de novo sem necessidade. Por baixo, o código do programa passou por uma grande reorganização: ela não muda nada no uso, mas deixa o WinZapp mais fácil de manter e de corrigir daqui para a frente. diff --git a/client/changelog_pt-PT.txt b/client/changelog_pt-PT.txt index 1b3e1a35d..99418d3e2 100644 --- a/client/changelog_pt-PT.txt +++ b/client/changelog_pt-PT.txt @@ -140,6 +140,8 @@ CORREÇÕES - Diálogo de reencaminhar: a seta para baixo no campo de pesquisa passa a ir para o primeiro resultado e a focar a lista, como na pesquisa de conversas. +- Contas comerciais verificadas do WhatsApp (bancos, aplicações de pagamento, empresas de serviços) que não guardou nos contactos apareciam na lista de conversas só com o número de telefone. Agora aparecem com o nome verificado da empresa, como no WhatsApp. Um nome guardado nos seus contactos continua a ter preferência. + V2.0.0.0 A grande novidade desta versão são as chamadas: agora é possível fazer, atender e recusar chamadas de voz e de vídeo no próprio WinZapp, sem pegar no telemóvel, com uma janela de chamada acessível, atalhos de teclado, definições de áudio próprias e o histórico no novo separador Chamadas. Chegaram também as conversas trancadas, a pesquisa nas conversas arquivadas, os idiomas turco e romeno e várias melhorias de utilização, além de correções em situações em que o WinZapp pedia para emparelhar de novo sem necessidade. Por baixo, o código do programa passou por uma grande reorganização: não muda nada na utilização, mas torna o WinZapp mais fácil de manter e de corrigir daqui para a frente. diff --git a/client/changelog_ro.txt b/client/changelog_ro.txt index b9cedbc65..8b67947d9 100644 --- a/client/changelog_ro.txt +++ b/client/changelog_ro.txt @@ -140,6 +140,8 @@ CORECTĂRI - Dialogul de redirecționare: săgeata în jos din câmpul de căutare merge acum la primul rezultat și mută focalizarea pe listă, ca în căutarea conversațiilor. +- Conturile de afaceri WhatsApp verificate (bănci, aplicații de plată, furnizori de utilități) pe care nu le-ați salvat în contacte apăreau în lista de conversații doar cu numărul de telefon. Acum apar cu numele verificat al companiei, ca în WhatsApp. Un nume salvat în contacte are în continuare prioritate. + V2.0.0.0 Marea noutate a acestei versiuni sunt apelurile: acum puteți iniția, prelua și respinge apeluri vocale și video direct din WinZapp, fără să luați telefonul, cu o fereastră de apel accesibilă, comenzi rapide de la tastatură, setări audio proprii și istoricul în noua filă, Apeluri. Alte noutăți apărute: conversațiile blocate, căutarea în conversațiile arhivate, limbile turcă și română și mai multe îmbunătățiri de utilizare, precum și corecții pentru situațiile în care WinZapp cerea o nouă asociere fără să fie nevoie. În spate, codul programului a trecut printr-o reorganizare amplă: nu schimbă nimic în utilizare, dar face WinZapp mai ușor de întreținut și de corectat de acum înainte. diff --git a/client/changelog_tr-TR.txt b/client/changelog_tr-TR.txt index af0e43725..7abb5f8ce 100644 --- a/client/changelog_tr-TR.txt +++ b/client/changelog_tr-TR.txt @@ -140,6 +140,8 @@ DÜZELTMELER - İlet penceresi: arama alanında aşağı ok artık ilk sonuca gidip odağı listeye taşıyor; sohbet aramasındaki gibi. +- Kişilerinize kaydetmediğiniz doğrulanmış WhatsApp Business hesapları (bankalar, ödeme uygulamaları, hizmet şirketleri) sohbet listesinde yalnızca telefon numarasıyla görünüyordu. Artık WhatsApp'ta olduğu gibi işletmenin doğrulanmış adıyla görünür. Kişilerinizde kayıtlı bir ad yine önceliklidir. + V2.0.0.0 Bu sürümün en büyük yeniliği aramalardır: Telefona ihtiyaç duymadan doğrudan WinZapp üzerinden sesli ve görüntülü arama başlatabilir, gelen aramaları yanıtlayabilir veya reddedebilirsiniz. Bu yenilik; tam erişilebilir bir arama penceresi, pratik klavye kısayolları, aramalara özel ses ayarları ve yeni Aramalar sekmesindeki kapsamlı arama geçmişiyle birlikte sunulmaktadır. Ayrıca PIN korumalı kilitli sohbetler, arşivlenmiş sohbetlerde arama, resmi Türkçe ve Rumence dil destekleri ile çeşitli kullanım iyileştirmeleri eklendi; WinZapp'in gereksiz yere yeniden eşleştirme istediği durumlar da düzeltildi. Arka planda programın kod tabanı büyük bir yeniden yapılandırmadan (refactoring) geçti: Bu durum WinZapp'i kullanım şeklinizde hiçbir şeyi değiştirmez; ancak programın uzun vadeli kararlılığını, bakımını ve hata düzeltme süreçlerini önemli ölçüde kolaylaştırır. diff --git a/client/main_window/chats_store.py b/client/main_window/chats_store.py index f84f0dbb6..e548f4b48 100644 --- a/client/main_window/chats_store.py +++ b/client/main_window/chats_store.py @@ -524,6 +524,15 @@ def _lift_contact_identity(chat: dict) -> None: so lastMessage is empty for all of them) and no unread count was dropped from the conversation list entirely. + A verified WhatsApp Business account (banks, payment apps, utilities) + is a second gap in the same mechanism: it sets no personal `pushname`, + so name/shortName/pushname all came up empty and the chat was listed + as a bare phone number. WhatsApp's own client shows the business's + verified name, which WPPConnect serialises as `contact.verifiedName`; + it is the last fallback for `name`, after the address-book name and + shortName. Seen live on accounts such as "99 Pay" and "Verisure + Brasil" (TeleZapp fork, reffehcsoibaf/winzapp). + Never overwrites a value the chat already carries — the top-level keys win when both are present. Mutates `chat` in place. """ @@ -531,7 +540,11 @@ def _lift_contact_identity(chat: dict) -> None: if not isinstance(contact_obj, dict): return if not (chat.get("name") or "").strip(): - cname = (contact_obj.get("name") or contact_obj.get("shortName") or "").strip() + cname = ( + contact_obj.get("name") or contact_obj.get("shortName") + or contact_obj.get("verifiedName") or "" + ) + cname = cname.strip() if isinstance(cname, str) else "" if cname: chat["name"] = cname if not (chat.get("pushName") or "").strip(): diff --git a/tests/test_chat_identity.py b/tests/test_chat_identity.py index 3c7c95bd5..bb78991ce 100644 --- a/tests/test_chat_identity.py +++ b/tests/test_chat_identity.py @@ -46,6 +46,30 @@ def test_accepts_either_pushname_spelling(self): lift(chat) assert chat["pushName"] == "Zé" + def test_falls_back_to_the_verified_business_name(self): + """A verified WhatsApp Business account (a bank, a payment app, a + utility) sets no personal pushname: name, shortName and pushname all + arrive empty and only contact.verifiedName carries the name + WhatsApp's own client shows.""" + chat = {"contact": {"verifiedName": "99 Pay", "isBusiness": True}} + lift(chat) + assert chat["name"] == "99 Pay" + assert "pushName" not in chat + + def test_name_and_short_name_still_win_over_the_verified_name(self): + chat = {"contact": {"name": "Meu Apelido", "verifiedName": "99 Pay"}} + lift(chat) + assert chat["name"] == "Meu Apelido" + chat = {"contact": {"shortName": "Apelido", "verifiedName": "99 Pay"}} + lift(chat) + assert chat["name"] == "Apelido" + + @pytest.mark.parametrize("bad", [None, 42, " ", {"x": 1}]) + def test_a_missing_or_malformed_verified_name_is_ignored(self, bad): + chat = {"contact": {"verifiedName": bad}} + lift(chat) + assert "name" not in chat + def test_never_overwrites_an_existing_top_level_name(self): chat = {"name": "Apelido meu", "contact": {"name": "Nome do contato"}} lift(chat) @@ -117,3 +141,54 @@ def test_raw_name_still_wins_when_present(self): def test_group_name_still_counts(self): assert _keeps_chat(**self._base(group_name="Equipe WinZapp")) is True + + +class _ResolverStub: + """Just what the real _resolve_contact_name() reads, for a chat whose + number is not in the address book.""" + + _resolve_contact_name = MainWindow._resolve_contact_name + _is_bad_contact_name = staticmethod(MainWindow._is_bad_contact_name) + + def __init__(self, contacts=None): + self.contacts = contacts or {} + self._presence_pushname_map = {} + self._phone_to_lid = {} + self._lid_to_phone = {} + + def _get_contact_tolerant(self, jid): + return self.contacts.get(jid) + + def _find_alt_jid_from_messages(self, chat): + return "" + + +class TestNotInTheAddressBook: + """The list shows a name instead of the phone number for a number that + was never saved: the verified name of a business, or the name a person + chose for their own WhatsApp profile. These pin the lift and the real + resolver together, so either regressing turns those rows back into + numbers.""" + + JID = "552123919910@s.whatsapp.net" + + def test_a_verified_business_is_named_by_the_resolver(self): + chat = {"remoteJid": self.JID, "contact": {"verifiedName": "99 Pay"}} + lift(chat) + assert _ResolverStub()._resolve_contact_name(chat) == "99 Pay" + + def test_an_address_book_name_still_wins_over_the_verified_name(self): + chat = {"remoteJid": self.JID, "contact": {"verifiedName": "99 Pay"}} + lift(chat) + stub = _ResolverStub({self.JID: {"name": "Carteira digital"}}) + assert stub._resolve_contact_name(chat) == "Carteira digital" + + def test_a_person_s_profile_name_is_lifted_for_the_list(self): + """Nothing in the address book, so the resolver has no name; the + chat list then uses chat["pushName"], which the lift fills from the + contact's own WhatsApp profile name.""" + chat = {"remoteJid": "5511999999999@s.whatsapp.net", + "contact": {"pushname": "Maria da Padaria"}} + lift(chat) + assert _ResolverStub()._resolve_contact_name(chat) is None + assert chat["pushName"] == "Maria da Padaria" From d7961344e0490bbb779bfef2c6d98cd5652fc29c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 22:17:56 +0000 Subject: [PATCH 2/2] Address review of the verified business name - A whitespace-only contact name or shortName no longer hides the verified name behind it: the lift takes the first non-blank string. - Romanian changelog: "Conturile business", the catalog's term for a business account. - Drop the assertion that the resolver returns None for a pushname-only chat; it pinned something outside this feature. - Docstring: keep the accounts it was seen on, drop the fork reference. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WmFGjY3WDiwbRjXL6zhiyY --- client/changelog_ro.txt | 2 +- client/main_window/chats_store.py | 14 +++++++++----- tests/test_chat_identity.py | 13 +++++++++---- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/client/changelog_ro.txt b/client/changelog_ro.txt index ba17dea63..98e7c145c 100644 --- a/client/changelog_ro.txt +++ b/client/changelog_ro.txt @@ -142,7 +142,7 @@ CORECTĂRI - Lista de țări de la asocierea prin număr de telefon conține acum toate țările și teritoriile acceptate de WhatsApp (lipseau, de exemplu, Țările de Jos, Kenya, Belarus și Macedonia de Nord), iar numele țărilor apar în limba aleasă și pentru poloneză, turcă și română. -- Conturile de afaceri WhatsApp verificate (bănci, aplicații de plată, furnizori de utilități) pe care nu le-ați salvat în contacte apăreau în lista de conversații doar cu numărul de telefon. Acum apar cu numele verificat al companiei, ca în WhatsApp. Un nume salvat în contacte are în continuare prioritate. +- Conturile business WhatsApp verificate (bănci, aplicații de plată, furnizori de utilități) pe care nu le-ați salvat în contacte apăreau în lista de conversații doar cu numărul de telefon. Acum apar cu numele verificat al companiei, ca în WhatsApp. Un nume salvat în contacte are în continuare prioritate. V2.0.0.0 diff --git a/client/main_window/chats_store.py b/client/main_window/chats_store.py index e548f4b48..363c58e6d 100644 --- a/client/main_window/chats_store.py +++ b/client/main_window/chats_store.py @@ -531,7 +531,7 @@ def _lift_contact_identity(chat: dict) -> None: verified name, which WPPConnect serialises as `contact.verifiedName`; it is the last fallback for `name`, after the address-book name and shortName. Seen live on accounts such as "99 Pay" and "Verisure - Brasil" (TeleZapp fork, reffehcsoibaf/winzapp). + Brasil". Never overwrites a value the chat already carries — the top-level keys win when both are present. Mutates `chat` in place. @@ -540,11 +540,15 @@ def _lift_contact_identity(chat: dict) -> None: if not isinstance(contact_obj, dict): return if not (chat.get("name") or "").strip(): - cname = ( - contact_obj.get("name") or contact_obj.get("shortName") - or contact_obj.get("verifiedName") or "" + # The first non-blank one: a whitespace-only name must not hide + # the verified name behind it. + cname = next( + (value.strip() + for value in (contact_obj.get(key) + for key in ("name", "shortName", "verifiedName")) + if isinstance(value, str) and value.strip()), + "", ) - cname = cname.strip() if isinstance(cname, str) else "" if cname: chat["name"] = cname if not (chat.get("pushName") or "").strip(): diff --git a/tests/test_chat_identity.py b/tests/test_chat_identity.py index bb78991ce..0abb4604b 100644 --- a/tests/test_chat_identity.py +++ b/tests/test_chat_identity.py @@ -64,6 +64,12 @@ def test_name_and_short_name_still_win_over_the_verified_name(self): lift(chat) assert chat["name"] == "Apelido" + @pytest.mark.parametrize("blank", ["name", "shortName"]) + def test_a_blank_address_book_name_does_not_hide_the_verified_name(self, blank): + chat = {"contact": {blank: " ", "verifiedName": "99 Pay"}} + lift(chat) + assert chat["name"] == "99 Pay" + @pytest.mark.parametrize("bad", [None, 42, " ", {"x": 1}]) def test_a_missing_or_malformed_verified_name_is_ignored(self, bad): chat = {"contact": {"verifiedName": bad}} @@ -184,11 +190,10 @@ def test_an_address_book_name_still_wins_over_the_verified_name(self): assert stub._resolve_contact_name(chat) == "Carteira digital" def test_a_person_s_profile_name_is_lifted_for_the_list(self): - """Nothing in the address book, so the resolver has no name; the - chat list then uses chat["pushName"], which the lift fills from the - contact's own WhatsApp profile name.""" + """Nothing in the address book; the chat list then uses + chat["pushName"], which the lift fills from the contact's own + WhatsApp profile name.""" chat = {"remoteJid": "5511999999999@s.whatsapp.net", "contact": {"pushname": "Maria da Padaria"}} lift(chat) - assert _ResolverStub()._resolve_contact_name(chat) is None assert chat["pushName"] == "Maria da Padaria"