diff --git a/apps/mobile/src/components/context-control.mounted.test.tsx b/apps/mobile/src/components/context-control.mounted.test.tsx index 821030f88d5..ae1690d01f3 100644 --- a/apps/mobile/src/components/context-control.mounted.test.tsx +++ b/apps/mobile/src/components/context-control.mounted.test.tsx @@ -43,6 +43,14 @@ const LIGHT_COLORS = { border: 'rgba(20, 15, 10, 0.09)', primary: '#4F5A10', }; +// The provider now announces the lost-membership recovery through the +// toast adapter, whose module graph reaches react-native's Flow source +// the mounted transform cannot parse. Suites mock the adapter (not the +// sonner package) so the mock sits at the edge that imports it. +vi.mock('@/lib/a11y/announcing-toast', () => ({ + announcingToast: { success: vi.fn(), error: vi.fn(), warning: vi.fn() }, +})); + vi.mock('@/lib/auth/auth-context', () => ({ useAuth: () => auth })); vi.mock('@/lib/auth/logout-cleanup', () => ({ unregisterActivityTokensAndTombstone: vi.fn() })); vi.mock('expo-secure-store', () => ({ @@ -259,7 +267,10 @@ describe.each(['ios', 'android'])('ContextControl on %s', os => { it.each([ { choice: 'Personal', expected: null }, { choice: name, expected: 'org-a' }, - { choice: 'Cancel', expected: 'org-missing' }, + // The stored organization is missing from the list, so the provider + // recovers to the first listed membership before the sheet opens; a + // Cancel is then a plain dismissal of that recovered scope. + { choice: 'Cancel', expected: 'org-a' }, ])( 'applies the picked account $choice without an implicit reset', async ({ choice, expected }) => { @@ -278,13 +289,18 @@ describe.each(['ios', 'android'])('ContextControl on %s', os => { storage.read.mockResolvedValue('org-missing'); const ui = await mount(); await waitFor(() => !picker(ui).props.disabled); + // The stored organization is missing from the list, so the provider has + // already recovered to the first listed membership before the sheet opens. + await waitFor( + () => ui.renderer.root.findByType('GlobalScope' as ElementType).props.id === 'org-a' + ); await openSheet(ui); // The native sheet's dismiss path, not a row: the scope stays as it was. await act(() => { (backdrop(ui).props.onClose as () => void)(); }); expect(rows(ui)).toHaveLength(0); - expect(ui.renderer.root.findByType('GlobalScope' as ElementType).props.id).toBe('org-missing'); + expect(ui.renderer.root.findByType('GlobalScope' as ElementType).props.id).toBe('org-a'); }); it('separates the rows and exposes the current account through the radio state', async () => { @@ -332,9 +348,10 @@ describe.each(['ios', 'android'])('ContextControl on %s', os => { it.each([ // An absent stored choice is 'not chosen yet': the provider's login default // resolves to the first organization, so the picker must mark that row. A - // stored organization missing from the list marks no row at all. + // stored organization missing from the list recovers to that first + // membership before the sheet opens, so its row is marked too. { stored: null, checked: name }, - { stored: 'org-missing', checked: undefined }, + { stored: 'org-missing', checked: name }, ])( 'marks the account the user is actually on when the stored one is missing (stored=$stored)', async ({ stored, checked }) => { @@ -347,7 +364,7 @@ describe.each(['ios', 'android'])('ContextControl on %s', os => { rows(ui) .filter(row => row.checked) .map(row => row.label) - ).toEqual(checked === undefined ? [] : [checked]); + ).toEqual([checked]); } ); @@ -395,18 +412,19 @@ describe.each(['ios', 'android'])('ContextControl on %s', os => { }); }); - it('recovers an unavailable organization through Personal after an empty membership result', async () => { + it('recovers a stored organization missing from an empty membership list to Personal', async () => { storage.read.mockResolvedValue('org-missing'); list.mockResolvedValue([]); const ui = await mount(); - await waitFor(() => texts(ui).includes('Organization unavailable')); - expect(texts(ui)).not.toContain('Retry'); - expect(ui.renderer.root.findByType('GlobalScope' as ElementType).props.id).toBe('org-missing'); + // The stored organization is missing from the fetched list, so the + // provider switches to Personal (no membership remains) on the first + // snapshot instead of rendering the dead selection's error surface. + await waitFor( + () => ui.renderer.root.findByType('GlobalScope' as ElementType).props.id === null + ); + expect(texts(ui)).not.toContain('Organization unavailable'); await openSheet(ui); expect(rows(ui).map(row => row.label)).toEqual(['Personal']); - await chooseRow(ui, 'Personal'); - expect(texts(ui)).toContain('Personal'); - expect(texts(ui)).not.toContain('Organization unavailable'); }); it.each([false, true])('retries only the failed name query, read-only=%s', async readOnly => { @@ -507,6 +525,9 @@ describe.each(['ios', 'android'])('ContextControl on %s', os => { it.each([ { organizationId: null, expected: 'Personal' }, { organizationId: 'org-a', expected: name }, + // The stored global scope ('global-org') is missing from the fetched + // list, so the provider recovers to the first listed membership; the + // explicit per-session scope stays read-only and independent of that. { organizationId: 'org-missing', expected: 'Organization unavailable' }, ])( 'keeps the resolved session scope $organizationId read-only and independent', @@ -515,17 +536,22 @@ describe.each(['ios', 'android'])('ContextControl on %s', os => { const ui = await mount({ scope: { organizationId, isResolved: true } }); await waitFor(() => texts(ui).includes(expected)); expect(ui.renderer.root.findAll(node => node.type === Pressable)).toHaveLength(0); - expect(ui.renderer.root.findByType('GlobalScope' as ElementType).props.id).toBe('global-org'); + expect(ui.renderer.root.findByType('GlobalScope' as ElementType).props.id).toBe('org-a'); } ); it('does not infer Personal or offer a picker for unresolved session scope', async () => { storage.read.mockResolvedValue('global-org'); const ui = await mount({ scope: { organizationId: null, isResolved: false } }); + // The stored global scope is missing from the list, so the provider + // recovers to the first listed membership; the unresolved session scope + // still infers nothing and offers no picker. + await waitFor( + () => ui.renderer.root.findByType('GlobalScope' as ElementType).props.id === 'org-a' + ); expect(texts(ui)).not.toContain('Personal'); expect(ui.renderer.root.findAll(node => node.type === Skeleton)).toHaveLength(1); expect(ui.renderer.root.findAll(node => node.type === Pressable)).toHaveLength(0); - expect(ui.renderer.root.findByType('GlobalScope' as ElementType).props.id).toBe('global-org'); }); it.each(['pending', 'failed'])( diff --git a/apps/mobile/src/components/pr-review/pr-review-context-line.mounted.test.tsx b/apps/mobile/src/components/pr-review/pr-review-context-line.mounted.test.tsx index e3999285f7d..8b2e61a0580 100644 --- a/apps/mobile/src/components/pr-review/pr-review-context-line.mounted.test.tsx +++ b/apps/mobile/src/components/pr-review/pr-review-context-line.mounted.test.tsx @@ -16,6 +16,14 @@ const list = vi.hoisted(() => vi.fn()); const storage = vi.hoisted(() => ({ read: vi.fn(), write: vi.fn(), remove: vi.fn() })); const auth = vi.hoisted(() => ({ token: 'token' as string | undefined })); +// The mounted OrganizationProvider announces a lost-membership switch through +// the toast adapter, whose module graph reaches react-native's Flow source the +// mounted transform cannot parse. The mock sits at the adapter, so the visual +// toast package never loads here. +vi.mock('@/lib/a11y/announcing-toast', () => ({ + announcingToast: { success: vi.fn(), error: vi.fn(), warning: vi.fn() }, +})); + vi.mock('@/lib/auth/auth-context', () => ({ useAuth: () => auth })); vi.mock('@/lib/auth/logout-cleanup', () => ({ unregisterActivityTokensAndTombstone: vi.fn() })); vi.mock('expo-secure-store', () => ({ @@ -201,10 +209,14 @@ describe('PrReviewContextLine', () => { expect(line(ui)).toBeDefined(); }); - it('names the unavailable scope when the persisted organization left the list', async () => { + it('recovers a persisted organization that left the list to the first membership', async () => { storage.read.mockResolvedValue('gone-org'); const ui = await mount(); - await waitFor(() => texts(ui).includes('For Organization')); - expect(texts(ui)).toContain('Organization unavailable'); + // The first settled list omits the stored organization, so the provider + // switches to the first membership and persists it instead of naming a + // dead selection the reader cannot act on. + await waitFor(() => texts(ui).includes('For Acme')); + expect(texts(ui)).not.toContain('Organization unavailable'); + expect(storage.write).toHaveBeenCalledWith(ORGANIZATION_STORAGE_KEY, 'org-a'); }); }); diff --git a/apps/mobile/src/components/profile-credits-card.mounted.test.tsx b/apps/mobile/src/components/profile-credits-card.mounted.test.tsx index 22fcc13fa82..f8f0fb25998 100644 --- a/apps/mobile/src/components/profile-credits-card.mounted.test.tsx +++ b/apps/mobile/src/components/profile-credits-card.mounted.test.tsx @@ -32,6 +32,14 @@ const currentUser = vi.hoisted(() => ({ /** Window the card lays out in; a narrow one must stack the credits header. */ const windowDims = vi.hoisted(() => ({ width: 390, height: 844, fontScale: 1, scale: 2 })); +// The provider now announces the lost-membership recovery through the +// toast adapter, whose module graph reaches react-native's Flow source +// the mounted transform cannot parse. Suites mock the adapter (not the +// sonner package) so the mock sits at the edge that imports it. +vi.mock('@/lib/a11y/announcing-toast', () => ({ + announcingToast: { success: vi.fn(), error: vi.fn(), warning: vi.fn() }, +})); + vi.mock('@/lib/trpc', () => ({ useTRPC: () => ({ user: { @@ -87,11 +95,17 @@ vi.mock('expo-router', () => ({ useRouter: () => ({ push: routerPush }), })); -// The mounted OrganizationProvider resolves its default from this list; an -// empty settled list keeps the card's own selection assertions unchanged. +// The mounted OrganizationProvider reads the shared memberships from this +// list. The payload is driveable per test: a stored selection missing from +// the fetched list now recovers to the first membership (or Personal), so a +// test staging a stale stored selection seeds `listState.payload` with a +// list that still names it. +const listState = vi.hoisted(() => ({ + payload: [] as { organizationId: string; organizationName: string; role: string }[], +})); vi.mock('@/lib/hooks/use-organizations-list', () => ({ useOrganizationsList: () => ({ - data: [], + data: listState.payload, isFetched: true, isFetching: false, isError: false, @@ -258,6 +272,9 @@ beforeEach(() => { savedMetadata.clear(); saveCompletion = undefined; addCreditsRowProps.latest = undefined; + // The shared memberships the provider reads default to none; tests that + // stage a stored selection set their own payload. + listState.payload = []; routerPush.mockReset(); storage.read.mockReset().mockImplementation(async (key: string) => { await Promise.resolve(); @@ -333,6 +350,7 @@ describe('CreditsCard balance state', () => { const orgs = [ { organizationId: 'org-a', organizationName: 'Acme', role: 'owner' }, ] as OrgListEntry[]; + listState.payload = orgs; try { const { texts, unmount } = await mountCard(createTestQueryClient(), orgs); try { @@ -358,16 +376,23 @@ describe('CreditsCard balance state', () => { label: organizationName, }, ])('uses supplied memberships for the picker: $expected', async state => { - savedMetadata.set(ORGANIZATION_STORAGE_KEY, 'missing-org'); - const { renderer, texts, unmount } = await mountCard( + // The staged stored selection is missing from the supplied list, so the + // provider recovers before the card renders: give the shared list the + // same payload the card receives, so the recovery lands on the selection + // the assertions then drive. + listState.payload = state.orgs as OrgListEntry[]; + const seededSelection = (state.orgs as OrgListEntry[])[0]?.organizationId; + savedMetadata.set(ORGANIZATION_STORAGE_KEY, seededSelection ?? 'missing-org'); + const { renderer, unmount } = await mountCard( createTestQueryClient(), state.orgs as OrgListEntry[] ); await openSheet(renderer); expect([...accountLabels(renderer), 'Cancel']).toEqual(state.options); await choose(renderer, 'Cancel'); - expect(savedMetadata.get(ORGANIZATION_STORAGE_KEY)).toBe('missing-org'); - expect(texts()).toContain('Organization'); + expect(savedMetadata.get(ORGANIZATION_STORAGE_KEY)).toBe( + (state.orgs as OrgListEntry[])[0]?.organizationId ?? undefined + ); await openSheet(renderer); await choose(renderer, state.label); expect(savedMetadata.get(ORGANIZATION_STORAGE_KEY)).toBe(state.expected ?? undefined); @@ -385,6 +410,10 @@ describe('CreditsCard balance state', () => { { organizationId: 'org-a', organizationName: 'Supplied organization', role: 'owner' }, { organizationId: 'org-b', organizationName: 'Latest organization', role: 'owner' }, ] as OrgListEntry[]; + // The staged 'previous-org' selection must stay in the fetched list, or + // the provider's lost-membership recovery switches it away before the + // save-failure flow under test runs. + listState.payload = orgs; storage.write.mockRejectedValueOnce(new Error('write failed')); const { renderer, texts, unmount } = await mountCard(createTestQueryClient(), orgs); await openSheet(renderer); @@ -663,6 +692,9 @@ describe('CreditsCard add-credits entry point', () => { const orgs = [ { organizationId: 'org-a', organizationName: 'Acme', role: 'owner' }, ] as OrgListEntry[]; + // The stored selection must stay in the fetched list, or the provider's + // lost-membership recovery switches it to Personal before the card reads it. + listState.payload = orgs; const { texts, unmount } = await mountCard(queryClient, orgs); await waitFor(() => addCreditsRowProps.latest?.url !== undefined); @@ -684,6 +716,8 @@ describe('CreditsCard add-credits entry point', () => { const orgs = [ { organizationId: 'org-a', organizationName: 'Acme', role: 'member' }, ] as OrgListEntry[]; + // Same seeding as the money-role case: keep the selection recoverable. + listState.payload = orgs; const { texts, unmount } = await mountCard(queryClient, orgs); await waitFor(() => texts().includes('Acme')); diff --git a/apps/mobile/src/i18n/locales/af.json b/apps/mobile/src/i18n/locales/af.json index 7f26bb09a19..fbc894e068d 100644 --- a/apps/mobile/src/i18n/locales/af.json +++ b/apps/mobile/src/i18n/locales/af.json @@ -3013,6 +3013,7 @@ "chooseAccount": "Kies 'n rekening", "loadErrorTitle": "Jou organisasies kon nie gelaai word nie.", + "membershipLostNotice": "Jy het nie meer toegang tot hierdie organisasie nie. Oorgeskakel na {{target}}.", "noOrganization": "Geen organisasie", "noOrganizationDescription": "Hierdie rekening is nie deel van 'n organisasie nie. Skep of sluit by een aan op die web om organisasiefunksies soos lede-uitnodigings en waarskuwings oor lae saldo te gebruik.", diff --git a/apps/mobile/src/i18n/locales/am.json b/apps/mobile/src/i18n/locales/am.json index 635ecc96368..9ea2cbf266f 100644 --- a/apps/mobile/src/i18n/locales/am.json +++ b/apps/mobile/src/i18n/locales/am.json @@ -3013,6 +3013,7 @@ "chooseAccount": "መለያ ይምረጡ", "loadErrorTitle": "ድርጅቶችዎን መጫን አልተቻለም።", + "membershipLostNotice": "ይህን ድርጅት ከእንግዲህ የመድረስ ፈቃድ የለዎትም። ወደ {{target}} ተቀይሯል።", "noOrganization": "ድርጅት የለም", "noOrganizationDescription": "ይህ መለያ የድርጅት አካል አይደለም። እንደ አባል ግብዣዎች እና ዝቅተኛ ቀሪ ሂሳብ ማንቂያዎች ያሉ የድርጅት ባህሪያትን ለመጠቀም በድር ላይ አንዱን ይፍጠሩ ወይም ይቀላቀሉ።", diff --git a/apps/mobile/src/i18n/locales/ar.json b/apps/mobile/src/i18n/locales/ar.json index 3a84480ccc7..b9227eca9fe 100644 --- a/apps/mobile/src/i18n/locales/ar.json +++ b/apps/mobile/src/i18n/locales/ar.json @@ -3165,6 +3165,7 @@ "chooseAccount": "اختيار حساب", "loadErrorTitle": "تعذّر تحميل مؤسساتك.", + "membershipLostNotice": "لم يعد لديك حق الوصول إلى هذه المؤسسة. تم التبديل إلى {{target}}.", "noOrganization": "لا توجد مؤسسة", "noOrganizationDescription": "هذا الحساب ليس جزءًا من مؤسسة. أنشئ مؤسسة أو انضم إلى واحدة على الموقع لاستخدام ميزات المؤسسة مثل دعوات الأعضاء وتنبيهات الرصيد المنخفض.", diff --git a/apps/mobile/src/i18n/locales/az.json b/apps/mobile/src/i18n/locales/az.json index 57504767a43..8832df4b778 100644 --- a/apps/mobile/src/i18n/locales/az.json +++ b/apps/mobile/src/i18n/locales/az.json @@ -3013,6 +3013,7 @@ "chooseAccount": "Hesab seç", "loadErrorTitle": "Təşkilatlarını yükləmək mümkün olmadı.", + "membershipLostNotice": "Bu təşkilata artıq giriş icazən yoxdur. Keçid edildi: {{target}}.", "noOrganization": "Təşkilat yoxdur", "noOrganizationDescription": "Bu hesab heç bir təşkilatın üzvü deyil. Üzv dəvətləri və aşağı balans xəbərdarlıqları kimi təşkilat funksiyalarından istifadə etmək üçün saytda təşkilat yarat və ya ona qoşul.", diff --git a/apps/mobile/src/i18n/locales/be.json b/apps/mobile/src/i18n/locales/be.json index a444c3e0964..86fc0202891 100644 --- a/apps/mobile/src/i18n/locales/be.json +++ b/apps/mobile/src/i18n/locales/be.json @@ -3093,6 +3093,7 @@ "chooseAccount": "Выберыце ўліковы запіс", "loadErrorTitle": "Не ўдалося загрузіць вашы арганізацыі.", + "membershipLostNotice": "У вас больш няма доступу да гэтай арганізацыі. Пераключана на {{target}}.", "noOrganization": "Няма арганізацыі", "noOrganizationDescription": "Гэты ўліковы запіс не ўваходзіць у арганізацыю. Стварыце арганізацыю або далучыцеся да яе на сайце, каб выкарыстоўваць функцыі арганізацыі, такія як запрашэнні ўдзельнікаў і апавяшчэнні аб нізкім балансе.", diff --git a/apps/mobile/src/i18n/locales/bg.json b/apps/mobile/src/i18n/locales/bg.json index 2bc2e6327ac..b4c6fc7279e 100644 --- a/apps/mobile/src/i18n/locales/bg.json +++ b/apps/mobile/src/i18n/locales/bg.json @@ -3013,6 +3013,7 @@ "chooseAccount": "Избери акаунт", "loadErrorTitle": "Не успяхме да заредим организациите ти.", + "membershipLostNotice": "Вече нямаш достъп до тази организация. Превключено към {{target}}.", "noOrganization": "Няма организация", "noOrganizationDescription": "Този акаунт не е част от организация. Създай или се присъедини към организация в сайта, за да използваш функции на организацията като покани за членове и известия за нисък баланс.", diff --git a/apps/mobile/src/i18n/locales/bn.json b/apps/mobile/src/i18n/locales/bn.json index 68fdc357cbd..7019bd96a83 100644 --- a/apps/mobile/src/i18n/locales/bn.json +++ b/apps/mobile/src/i18n/locales/bn.json @@ -3013,6 +3013,7 @@ "chooseAccount": "একটি অ্যাকাউন্ট নির্বাচন করুন", "loadErrorTitle": "আপনার সংস্থাগুলো লোড করা যায়নি।", + "membershipLostNotice": "এই সংস্থায় আপনার আর অ্যাক্সেস নেই। স্যুইচ করা হয়েছে {{target}}-এ।", "noOrganization": "কোনো সংস্থা নেই", "noOrganizationDescription": "এই অ্যাকাউন্টটি কোনো সংস্থার অংশ নয়। সদস্য আমন্ত্রণ এবং কম ব্যালেন্স সতর্কতার মতো সংস্থার বৈশিষ্ট্য ব্যবহার করতে ওয়েবে একটি সংস্থা তৈরি করুন বা যোগ দিন।", diff --git a/apps/mobile/src/i18n/locales/bs.json b/apps/mobile/src/i18n/locales/bs.json index 6cee38aa91e..75146a5a8ec 100644 --- a/apps/mobile/src/i18n/locales/bs.json +++ b/apps/mobile/src/i18n/locales/bs.json @@ -3053,6 +3053,7 @@ "chooseAccount": "Odaberi račun", "loadErrorTitle": "Nije moguće učitati tvoje organizacije.", + "membershipLostNotice": "Više nemaš pristup ovoj organizaciji. Prebačeno na {{target}}.", "noOrganization": "Nema organizacije", "noOrganizationDescription": "Ovaj račun nije dio organizacije. Kreiraj ili se pridruži organizaciji na webu da bi koristio funkcije organizacije kao što su pozivnice za članove i upozorenja o niskom saldu.", diff --git a/apps/mobile/src/i18n/locales/ca.json b/apps/mobile/src/i18n/locales/ca.json index 8dfbe0a81a3..6fb1429b8f6 100644 --- a/apps/mobile/src/i18n/locales/ca.json +++ b/apps/mobile/src/i18n/locales/ca.json @@ -3053,6 +3053,7 @@ "chooseAccount": "Selecciona un compte", "loadErrorTitle": "No s'han pogut carregar les teves organitzacions.", + "membershipLostNotice": "Ja no tens accés a aquesta organització. Canviat a {{target}}.", "noOrganization": "Sense organització", "noOrganizationDescription": "Aquest compte no forma part de cap organització. Crea'n una o uneix-te'n a una al web per utilitzar funcions d'organització com les invitacions de membres i les alertes de saldo baix.", diff --git a/apps/mobile/src/i18n/locales/ckb.json b/apps/mobile/src/i18n/locales/ckb.json index a08d734dc0e..14f20094ae1 100644 --- a/apps/mobile/src/i18n/locales/ckb.json +++ b/apps/mobile/src/i18n/locales/ckb.json @@ -3013,6 +3013,7 @@ "chooseAccount": "هەژمارێک هەڵبژێرە", "loadErrorTitle": "نەتوانرا ڕێکخراوەکانت بار بکرێن.", + "membershipLostNotice": "چیتر مۆڵەتی دەستگەیشتن بەم ڕێکخراوەت نییە. گۆڕدرا بۆ {{target}}.", "noOrganization": "ڕێکخراو نییە", "noOrganizationDescription": "ئەم هەژمارە بەشێک نییە لە ڕێکخراوێک. لە وێبدا ڕێکخراوێک دروست بکە یان پەیوەندی پێوە بکە بۆ بەکارهێنانی تایبەتمەندییەکانی ڕێکخراو وەک بانگهێشتی ئەندامان و ئاگادارکەرەوەکانی باڵانسی کەم.", diff --git a/apps/mobile/src/i18n/locales/cs.json b/apps/mobile/src/i18n/locales/cs.json index 8da26094678..f77239a768b 100644 --- a/apps/mobile/src/i18n/locales/cs.json +++ b/apps/mobile/src/i18n/locales/cs.json @@ -3093,6 +3093,7 @@ "chooseAccount": "Vyberte účet", "loadErrorTitle": "Vaše organizace se nepodařilo načíst.", + "membershipLostNotice": "Už nemáte přístup do této organizace. Přepnuto na {{target}}.", "noOrganization": "Žádná organizace", "noOrganizationDescription": "Tento účet není součástí organizace. Vytvořte organizaci nebo se k některé připojte na webu, abyste mohli využívat funkce organizace, jako jsou pozvánky pro členy a upozornění na nízký zůstatek.", diff --git a/apps/mobile/src/i18n/locales/cy.json b/apps/mobile/src/i18n/locales/cy.json index ea6b44eb9bf..f2883ee5d9f 100644 --- a/apps/mobile/src/i18n/locales/cy.json +++ b/apps/mobile/src/i18n/locales/cy.json @@ -3173,6 +3173,7 @@ "chooseAccount": "Dewiswch gyfrif", "loadErrorTitle": "Methu llwytho'ch sefydliadau.", + "membershipLostNotice": "Nid oes gennych fynediad i'r sefydliad hwn mwyach. Wedi newid i {{target}}.", "noOrganization": "Dim sefydliad", "noOrganizationDescription": "Nid yw'r cyfrif hwn yn rhan o sefydliad. Crëwch un neu ymunwch ag un ar y we i ddefnyddio nodweddion sefydliad fel gwahoddiadau i aelodau a rhybuddion balans isel.", diff --git a/apps/mobile/src/i18n/locales/da.json b/apps/mobile/src/i18n/locales/da.json index 9e0d2462860..5e2ddc93d89 100644 --- a/apps/mobile/src/i18n/locales/da.json +++ b/apps/mobile/src/i18n/locales/da.json @@ -3013,6 +3013,7 @@ "chooseAccount": "Vælg en konto", "loadErrorTitle": "Kunne ikke indlæse dine organisationer.", + "membershipLostNotice": "Du har ikke længere adgang til denne organisation. Skiftet til {{target}}.", "noOrganization": "Ingen organisation", "noOrganizationDescription": "Denne konto er ikke en del af en organisation. Opret eller tilslut dig en organisation på hjemmesiden for at bruge organisationsfunktioner som medlemsinvitationer og advarsler om lav saldo.", diff --git a/apps/mobile/src/i18n/locales/de.json b/apps/mobile/src/i18n/locales/de.json index bfdd50a81c5..86ec1681488 100644 --- a/apps/mobile/src/i18n/locales/de.json +++ b/apps/mobile/src/i18n/locales/de.json @@ -3005,6 +3005,7 @@ "chooseAccount": "Konto auswählen", "loadErrorTitle": "Deine Organisationen konnten nicht geladen werden.", + "membershipLostNotice": "Du hast keinen Zugriff mehr auf diese Organisation. Gewechselt zu {{target}}.", "noOrganization": "Keine Organisation", "noOrganizationDescription": "Dieses Konto gehört keiner Organisation an. Erstelle eine Organisation oder tritt einer auf der Website bei, um Organisationsfunktionen wie Mitgliedseinladungen und Warnungen bei niedrigem Guthaben zu nutzen.", diff --git a/apps/mobile/src/i18n/locales/el.json b/apps/mobile/src/i18n/locales/el.json index d2c27b68fac..38934eb49e8 100644 --- a/apps/mobile/src/i18n/locales/el.json +++ b/apps/mobile/src/i18n/locales/el.json @@ -3013,6 +3013,7 @@ "chooseAccount": "Επιλογή λογαριασμού", "loadErrorTitle": "Δεν ήταν δυνατή η φόρτωση των οργανισμών σου.", + "membershipLostNotice": "Δεν έχεις πλέον πρόσβαση σε αυτόν τον οργανισμό. Έγινε αλλαγή σε {{target}}.", "noOrganization": "Κανένας οργανισμός", "noOrganizationDescription": "Αυτός ο λογαριασμός δεν ανήκει σε οργανισμό. Δημιούργησε ή συμμετάσχε σε έναν οργανισμό στην ιστοσελίδα για να χρησιμοποιήσεις λειτουργίες οργανισμού όπως προσκλήσεις μελών και ειδοποιήσεις χαμηλού υπολοίπου.", diff --git a/apps/mobile/src/i18n/locales/en.json b/apps/mobile/src/i18n/locales/en.json index cf97e783d61..f4f5a9c7d45 100644 --- a/apps/mobile/src/i18n/locales/en.json +++ b/apps/mobile/src/i18n/locales/en.json @@ -3090,6 +3090,7 @@ "chooseAccount": "Choose an account", "loadErrorMessage": "Check your connection and try again.", "loadErrorTitle": "Couldn't load your organizations", + "membershipLostNotice": "You no longer have access to this organization. Switched to {{target}}.", "noOrganization": "No organization", "noOrganizationDescription": "This account is not part of an organization. Create or join one on the web to use organization features like member invites and low-balance alerts.", "openWebOrganizations": "Open organizations on web", diff --git a/apps/mobile/src/i18n/locales/es.json b/apps/mobile/src/i18n/locales/es.json index 829d8b8fab8..1ddd3b2b2cd 100644 --- a/apps/mobile/src/i18n/locales/es.json +++ b/apps/mobile/src/i18n/locales/es.json @@ -3045,6 +3045,7 @@ "chooseAccount": "Elegir una cuenta", "loadErrorTitle": "No se pudieron cargar tus organizaciones.", + "membershipLostNotice": "Ya no tienes acceso a esta organización. Cambiado a {{target}}.", "noOrganization": "Sin organización", "noOrganizationDescription": "Esta cuenta no forma parte de una organización. Crea una o únete a una en la web para usar funciones de organización como invitaciones de miembros y alertas de saldo bajo.", diff --git a/apps/mobile/src/i18n/locales/et.json b/apps/mobile/src/i18n/locales/et.json index fcd99b42fcf..252a76bff38 100644 --- a/apps/mobile/src/i18n/locales/et.json +++ b/apps/mobile/src/i18n/locales/et.json @@ -3013,6 +3013,7 @@ "chooseAccount": "Vali konto", "loadErrorTitle": "Sinu organisatsioone ei õnnestunud laadida.", + "membershipLostNotice": "Sul puudub enam juurdepääs sellele organisatsioonile. Vahetatud {{target}}-le.", "noOrganization": "Organisatsioon puudub", "noOrganizationDescription": "See konto ei kuulu ühtegi organisatsiooni. Organisatsiooni funktsioonide, nagu liikmekutsed ja väikese saldo hoiatused, kasutamiseks loo organisatsioon või liitu sellega veebis.", diff --git a/apps/mobile/src/i18n/locales/eu.json b/apps/mobile/src/i18n/locales/eu.json index 0f919b2f796..babcd2ea922 100644 --- a/apps/mobile/src/i18n/locales/eu.json +++ b/apps/mobile/src/i18n/locales/eu.json @@ -3013,6 +3013,7 @@ "chooseAccount": "Hautatu kontu bat", "loadErrorTitle": "Ezin izan dira zure erakundeak kargatu.", + "membershipLostNotice": "Ez duzu jada erakunde honetarako sarbiderik. Aldatu da {{target}}-ra.", "noOrganization": "Erakunderik ez", "noOrganizationDescription": "Kontu hau ez da erakunde baten parte. Sortu edo batu erakunde bat webgunean, kideen gonbidapenak eta saldo baxuaren abisuak bezalako erakunde-funtzioak erabiltzeko.", diff --git a/apps/mobile/src/i18n/locales/fa.json b/apps/mobile/src/i18n/locales/fa.json index 7f0fa7ea621..81d24bc4094 100644 --- a/apps/mobile/src/i18n/locales/fa.json +++ b/apps/mobile/src/i18n/locales/fa.json @@ -3013,6 +3013,7 @@ "chooseAccount": "انتخاب حساب", "loadErrorTitle": "بارگذاری سازمان‌های شما ممکن نشد.", + "membershipLostNotice": "شما دیگر به این سازمان دسترسی ندارید. به {{target}} تغییر یافت.", "noOrganization": "بدون سازمان", "noOrganizationDescription": "این حساب بخشی از هیچ سازمانی نیست. برای استفاده از ویژگی‌های سازمان مانند دعوت اعضا و هشدارهای موجودی کم، در وب یک سازمان ایجاد کنید یا به یکی بپیوندید.", diff --git a/apps/mobile/src/i18n/locales/fi.json b/apps/mobile/src/i18n/locales/fi.json index 543d7110c5d..7f1d484f198 100644 --- a/apps/mobile/src/i18n/locales/fi.json +++ b/apps/mobile/src/i18n/locales/fi.json @@ -3013,6 +3013,7 @@ "chooseAccount": "Valitse tili", "loadErrorTitle": "Organisaatioitasi ei voitu ladata.", + "membershipLostNotice": "Sinulla ei ole enää pääsyä tähän organisaatioon. Vaihdettu {{target}}.", "noOrganization": "Ei organisaatiota", "noOrganizationDescription": "Tämä tili ei kuulu mihinkään organisaatioon. Luo organisaatio tai liity sellaiseen verkossa käyttääksesi organisaatio-ominaisuuksia, kuten jäsenkutsuja ja alhaisen saldon hälytyksiä.", diff --git a/apps/mobile/src/i18n/locales/fil.json b/apps/mobile/src/i18n/locales/fil.json index d8542fcbe0f..1060994c31b 100644 --- a/apps/mobile/src/i18n/locales/fil.json +++ b/apps/mobile/src/i18n/locales/fil.json @@ -3013,6 +3013,7 @@ "chooseAccount": "Pumili ng account", "loadErrorTitle": "Hindi ma-load ang iyong mga organisasyon.", + "membershipLostNotice": "Wala ka nang access sa organisasyong ito. Lumipat sa {{target}}.", "noOrganization": "Walang organisasyon", "noOrganizationDescription": "Ang account na ito ay hindi bahagi ng isang organisasyon. Gumawa o sumali sa isang organisasyon sa web upang magamit ang mga feature ng organisasyon tulad ng mga imbitasyon ng miyembro at mga alerto sa mababang balanse.", diff --git a/apps/mobile/src/i18n/locales/fr.json b/apps/mobile/src/i18n/locales/fr.json index 5c11b611e2c..4f3e2b9a3f4 100644 --- a/apps/mobile/src/i18n/locales/fr.json +++ b/apps/mobile/src/i18n/locales/fr.json @@ -3045,6 +3045,7 @@ "chooseAccount": "Choisir un compte", "loadErrorTitle": "Impossible de charger vos organisations.", + "membershipLostNotice": "Vous n'avez plus accès à cette organisation. Basculé vers {{target}}.", "noOrganization": "Aucune organisation", "noOrganizationDescription": "Ce compte ne fait partie d'aucune organisation. Créez une organisation ou rejoignez-en une sur le site web pour utiliser les fonctionnalités d'organisation telles que les invitations de membres et les alertes de solde faible.", diff --git a/apps/mobile/src/i18n/locales/ga.json b/apps/mobile/src/i18n/locales/ga.json index dc13ce935e8..0d5a3de6bcb 100644 --- a/apps/mobile/src/i18n/locales/ga.json +++ b/apps/mobile/src/i18n/locales/ga.json @@ -3133,6 +3133,7 @@ "chooseAccount": "Roghnaigh cuntas", "loadErrorTitle": "Níorbh fhéidir d'eagraíochtaí a lódáil.", + "membershipLostNotice": "Níl rochtain agat ar an eagraíocht seo a thuilleadh. Athraithe go {{target}}.", "noOrganization": "Gan eagraíocht", "noOrganizationDescription": "Níl an cuntas seo mar chuid d'eagraíocht. Cruthaigh eagraíocht nó bí páirteach i gceann ar an ngréasán chun gnéithe eagraíochta a úsáid ar nós cuirí do bhaill agus foláirimh maidir le hiarmhéid íseal.", diff --git a/apps/mobile/src/i18n/locales/gl.json b/apps/mobile/src/i18n/locales/gl.json index b55bf1ffbd4..a756e6962bf 100644 --- a/apps/mobile/src/i18n/locales/gl.json +++ b/apps/mobile/src/i18n/locales/gl.json @@ -3013,6 +3013,7 @@ "chooseAccount": "Seleccionar unha conta", "loadErrorTitle": "Non se puideron cargar as túas organizacións.", + "membershipLostNotice": "Xa non tes acceso a esta organización. Cambiado a {{target}}.", "noOrganization": "Sen organización", "noOrganizationDescription": "Esta conta non forma parte dunha organización. Crea unha ou únete a unha na web para usar funcións de organización como as invitacións de membros e as alertas de saldo baixo.", diff --git a/apps/mobile/src/i18n/locales/gu.json b/apps/mobile/src/i18n/locales/gu.json index a3c3e1de785..66146e78deb 100644 --- a/apps/mobile/src/i18n/locales/gu.json +++ b/apps/mobile/src/i18n/locales/gu.json @@ -3013,6 +3013,7 @@ "chooseAccount": "એકાઉન્ટ પસંદ કરો", "loadErrorTitle": "તમારી સંસ્થાઓ લોડ કરી શકાઈ નહીં.", + "membershipLostNotice": "તમારી પાસે આ સંસ્થાનો ઍક્સેસ હવે નથી. {{target}} પર સ્વિચ કર્યું.", "noOrganization": "કોઈ સંસ્થા નથી", "noOrganizationDescription": "આ એકાઉન્ટ કોઈ સંસ્થાનો ભાગ નથી. સભ્ય આમંત્રણો અને ઓછી બેલેન્સ ચેતવણીઓ જેવી સંસ્થા સુવિધાઓનો ઉપયોગ કરવા માટે વેબ પર સંસ્થા બનાવો અથવા એકમાં જોડાઓ.", diff --git a/apps/mobile/src/i18n/locales/ha.json b/apps/mobile/src/i18n/locales/ha.json index dcf48d45ca2..381cb47fe1e 100644 --- a/apps/mobile/src/i18n/locales/ha.json +++ b/apps/mobile/src/i18n/locales/ha.json @@ -3013,6 +3013,7 @@ "chooseAccount": "Zaɓi asusu", "loadErrorTitle": "An kasa loda ƙungiyoyinka.", + "membershipLostNotice": "Ba ka da izinin shiga wannan ƙungiyar a yanzu. An sauya zuwa {{target}}.", "noOrganization": "Babu ƙungiya", "noOrganizationDescription": "Wannan asusun ba ya cikin wata ƙungiya. Ƙirƙiri ko shiga wata ƙungiya a yanar gizo don amfani da fasalolin ƙungiya kamar gayyata membobi da faɗakarwar ƙarancin ma'auni.", diff --git a/apps/mobile/src/i18n/locales/he.json b/apps/mobile/src/i18n/locales/he.json index eb6ba0ce775..0fd1b6af74c 100644 --- a/apps/mobile/src/i18n/locales/he.json +++ b/apps/mobile/src/i18n/locales/he.json @@ -3045,6 +3045,7 @@ "chooseAccount": "בחירת חשבון", "loadErrorTitle": "לא ניתן היה לטעון את הארגונים שלך.", + "membershipLostNotice": "אין לך גישה לארגון זה עוד. הוחלף ל-{{target}}.", "noOrganization": "אין ארגון", "noOrganizationDescription": "החשבון הזה אינו חלק מארגון. צור ארגון או הצטרף לאחד באתר כדי להשתמש בתכונות של ארגון כמו הזמנת חברים והתראות על יתרה נמוכה.", diff --git a/apps/mobile/src/i18n/locales/hi.json b/apps/mobile/src/i18n/locales/hi.json index 163b1bccb68..93284bc5528 100644 --- a/apps/mobile/src/i18n/locales/hi.json +++ b/apps/mobile/src/i18n/locales/hi.json @@ -3005,6 +3005,7 @@ "chooseAccount": "एक खाता चुनें", "loadErrorTitle": "आपके संगठन लोड नहीं हो सके।", + "membershipLostNotice": "आपको इस संगठन का ऐक्सेस अब नहीं है। {{target}} पर स्विच किया गया।", "noOrganization": "कोई संगठन नहीं", "noOrganizationDescription": "यह खाता किसी संगठन का हिस्सा नहीं है। सदस्य आमंत्रण और कम बैलेंस अलर्ट जैसी संगठन सुविधाओं का उपयोग करने के लिए वेब पर एक संगठन बनाएँ या उसमें शामिल हों।", diff --git a/apps/mobile/src/i18n/locales/hr.json b/apps/mobile/src/i18n/locales/hr.json index 0cb86376aa9..2f2c07c1a8a 100644 --- a/apps/mobile/src/i18n/locales/hr.json +++ b/apps/mobile/src/i18n/locales/hr.json @@ -3053,6 +3053,7 @@ "chooseAccount": "Odaberi račun", "loadErrorTitle": "Nije moguće učitati tvoje organizacije.", + "membershipLostNotice": "Više nemaš pristup ovoj organizaciji. Prebačeno na {{target}}.", "noOrganization": "Nema organizacije", "noOrganizationDescription": "Ovaj račun nije dio organizacije. Stvori ili se pridruži organizaciji na webu da bi koristio značajke organizacije kao što su pozivnice za članove i upozorenja o niskom saldu.", diff --git a/apps/mobile/src/i18n/locales/ht.json b/apps/mobile/src/i18n/locales/ht.json index a9bce57476b..3dc1a84e935 100644 --- a/apps/mobile/src/i18n/locales/ht.json +++ b/apps/mobile/src/i18n/locales/ht.json @@ -3013,6 +3013,7 @@ "chooseAccount": "Chwazi yon kont", "loadErrorTitle": "Nou pa t ka chaje òganizasyon ou yo.", + "membershipLostNotice": "Ou pa gen aksè nan òganizasyon sa a ankò. Chanje pou {{target}}.", "noOrganization": "Pa gen òganizasyon", "noOrganizationDescription": "Kont sa a pa fè pati yon òganizasyon. Kreye oswa rantre nan yon òganizasyon sou sit entènèt la pou itilize fonksyon òganizasyon tankou envitasyon manm ak alèt sou balans ki ba.", diff --git a/apps/mobile/src/i18n/locales/hu.json b/apps/mobile/src/i18n/locales/hu.json index 757466cc4b9..ae91dc0834d 100644 --- a/apps/mobile/src/i18n/locales/hu.json +++ b/apps/mobile/src/i18n/locales/hu.json @@ -3013,6 +3013,7 @@ "chooseAccount": "Fiók kiválasztása", "loadErrorTitle": "Nem sikerült betölteni a szervezeteidet.", + "membershipLostNotice": "Már nincs hozzáférésed ehhez a szervezethez. Átváltva a következőre: {{target}}.", "noOrganization": "Nincs szervezet", "noOrganizationDescription": "Ez a fiók nem tagja egyetlen szervezetnek sem. Hozz létre vagy csatlakozz egy szervezethez a weboldalon, hogy használhasd a szervezetfunkciókat, például a tagmeghívókat és az alacsony egyenlegre vonatkozó figyelmeztetéseket.", diff --git a/apps/mobile/src/i18n/locales/hy.json b/apps/mobile/src/i18n/locales/hy.json index cfa3cba7378..40882795092 100644 --- a/apps/mobile/src/i18n/locales/hy.json +++ b/apps/mobile/src/i18n/locales/hy.json @@ -3013,6 +3013,7 @@ "chooseAccount": "Ընտրել հաշիվ", "loadErrorTitle": "Չհաջողվեց բեռնել ձեր կազմակերպությունները։", + "membershipLostNotice": "Դուք այլևս այս կազմակերպություն մուտք գործելու իրավունք չունեք։ Փոխարկվել է {{target}}։", "noOrganization": "Կազմակերպություն չկա", "noOrganizationDescription": "Այս հաշիվը որևէ կազմակերպության մաս չէ։ Կազմակերպության գործառույթներից, ինչպիսիք են անդամների հրավերները և ցածր մնացորդի ծանուցումները, օգտվելու համար կայքում ստեղծեք կազմակերպություն կամ միացեք դրան։", diff --git a/apps/mobile/src/i18n/locales/id.json b/apps/mobile/src/i18n/locales/id.json index 96bd4834971..aa1c6be0f4d 100644 --- a/apps/mobile/src/i18n/locales/id.json +++ b/apps/mobile/src/i18n/locales/id.json @@ -3005,6 +3005,7 @@ "chooseAccount": "Pilih akun", "loadErrorTitle": "Tidak dapat memuat daftar organisasimu.", + "membershipLostNotice": "Kamu tidak lagi memiliki akses ke organisasi ini. Beralih ke {{target}}.", "noOrganization": "Tidak ada organisasi", "noOrganizationDescription": "Akun ini bukan bagian dari organisasi. Buat atau gabung ke organisasi di web untuk menggunakan fitur organisasi seperti undangan anggota dan peringatan saldo rendah.", diff --git a/apps/mobile/src/i18n/locales/ig.json b/apps/mobile/src/i18n/locales/ig.json index 9234ee37929..32150ff81bc 100644 --- a/apps/mobile/src/i18n/locales/ig.json +++ b/apps/mobile/src/i18n/locales/ig.json @@ -3013,6 +3013,7 @@ "chooseAccount": "Họrọ akaụntụ", "loadErrorTitle": "Enweghị ike ibudata ụlọ ọrụ gị.", + "membershipLostNotice": "Ị nweghịzi ikike ịbanye n'ụlọ ọrụ a. A gbanwere gaa na {{target}}.", "noOrganization": "Enweghị ụlọ ọrụ", "noOrganizationDescription": "Akaụntụ a esonyeghị na ụlọ ọrụ. Mepụta ma ọ bụ sonye na ụlọ ọrụ na webụ iji jiri atụmatụ ụlọ ọrụ dị ka ịkpọ ndị otu oku na ịdọ aka ná ntị gbasara obere ego n'akaụntụ.", diff --git a/apps/mobile/src/i18n/locales/is.json b/apps/mobile/src/i18n/locales/is.json index bb3dbcb5eca..96ae4d53676 100644 --- a/apps/mobile/src/i18n/locales/is.json +++ b/apps/mobile/src/i18n/locales/is.json @@ -3013,6 +3013,7 @@ "chooseAccount": "Veldu reikning", "loadErrorTitle": "Ekki tókst að hlaða félögunum þínum.", + "membershipLostNotice": "Þú hefur ekki lengur aðgang að þessu félagi. Skipt yfir í {{target}}.", "noOrganization": "Ekkert félag", "noOrganizationDescription": "Þessi reikningur er ekki hluti af félagi. Búðu til eða gakktu til liðs við félag á vefnum til að nota eiginleika félagsins eins og boð til meðlima og viðvaranir um lága stöðu.", diff --git a/apps/mobile/src/i18n/locales/it.json b/apps/mobile/src/i18n/locales/it.json index 1fd289e9f92..20bd132bc2e 100644 --- a/apps/mobile/src/i18n/locales/it.json +++ b/apps/mobile/src/i18n/locales/it.json @@ -3045,6 +3045,7 @@ "chooseAccount": "Seleziona un account", "loadErrorTitle": "Impossibile caricare le tue organizzazioni.", + "membershipLostNotice": "Non hai più accesso a questa organizzazione. Passato a {{target}}.", "noOrganization": "Nessuna organizzazione", "noOrganizationDescription": "Questo account non fa parte di un'organizzazione. Crea o unisciti a un'organizzazione sul web per usare le funzioni dell'organizzazione come gli inviti dei membri e gli avvisi di saldo basso.", diff --git a/apps/mobile/src/i18n/locales/ja.json b/apps/mobile/src/i18n/locales/ja.json index 4689673411d..0d3c67ec54f 100644 --- a/apps/mobile/src/i18n/locales/ja.json +++ b/apps/mobile/src/i18n/locales/ja.json @@ -3005,6 +3005,7 @@ "chooseAccount": "アカウントを選択", "loadErrorTitle": "組織を読み込めませんでした。", + "membershipLostNotice": "この組織にアクセスできなくなりました。{{target}}に切り替えました。", "noOrganization": "組織なし", "noOrganizationDescription": "このアカウントは組織に属していません。メンバーの招待や残高不足のアラートなどの組織機能を使用するには、ウェブで組織を作成するか参加してください。", diff --git a/apps/mobile/src/i18n/locales/ka.json b/apps/mobile/src/i18n/locales/ka.json index 9e580a79383..ed71f0ffdd8 100644 --- a/apps/mobile/src/i18n/locales/ka.json +++ b/apps/mobile/src/i18n/locales/ka.json @@ -3013,6 +3013,7 @@ "chooseAccount": "ანგარიშის არჩევა", "loadErrorTitle": "თქვენი ორგანიზაციების ჩატვირთვა ვერ მოხერხდა.", + "membershipLostNotice": "თქვენ აღარ გაქვთ წვდომა ამ ორგანიზაციაზე. გადავერთეთ {{target}}-ზე.", "noOrganization": "ორგანიზაცია არ არის", "noOrganizationDescription": "ეს ანგარიში არ არის ორგანიზაციის ნაწილი. ორგანიზაციის ფუნქციების, როგორიცაა წევრების მოწვევა და დაბალი ბალანსის შეტყობინებები, გამოსაყენებლად შექმენით ორგანიზაცია ან შეუერთდით მას ვებსაიტზე.", diff --git a/apps/mobile/src/i18n/locales/kk.json b/apps/mobile/src/i18n/locales/kk.json index e8ea32ba9ef..b68be47d030 100644 --- a/apps/mobile/src/i18n/locales/kk.json +++ b/apps/mobile/src/i18n/locales/kk.json @@ -3013,6 +3013,7 @@ "chooseAccount": "Аккаунт таңдау", "loadErrorTitle": "Ұйымдарыңызды жүктеу мүмкін болмады.", + "membershipLostNotice": "Бұл ұйымға енді кіру рұқсатыңыз жоқ. {{target}}-ға ауыстырылды.", "noOrganization": "Ұйым жоқ", "noOrganizationDescription": "Бұл аккаунт ұйымның бөлігі емес. Мүшелерді шақыру және төмен баланс туралы ескертулер сияқты ұйым мүмкіндіктерін пайдалану үшін браузерде ұйым құрыңыз немесе оған қосылыңыз.", diff --git a/apps/mobile/src/i18n/locales/km.json b/apps/mobile/src/i18n/locales/km.json index a73228e6c42..82da8890357 100644 --- a/apps/mobile/src/i18n/locales/km.json +++ b/apps/mobile/src/i18n/locales/km.json @@ -3013,6 +3013,7 @@ "chooseAccount": "ជ្រើសរើសគណនី", "loadErrorTitle": "មិនអាចផ្ទុកអង្គការរបស់អ្នកបានទេ។", + "membershipLostNotice": "អ្នកលែងមានសិទ្ធិចូលប្រើអង្គការនេះទៀតហើយ។ បានប្ដូរទៅ {{target}}។", "noOrganization": "គ្មានអង្គការ", "noOrganizationDescription": "គណនីនេះមិនមែនជាផ្នែកនៃអង្គការទេ។ បង្កើត ឬចូលរួមអង្គការមួយនៅលើគេហទំព័រ ដើម្បីប្រើមុខងារអង្គការដូចជាការអញ្ជើញសមាជិក និងការជូនដំណឹងអំពីសមតុល្យទាប។", diff --git a/apps/mobile/src/i18n/locales/kn.json b/apps/mobile/src/i18n/locales/kn.json index e33c6a46445..ac395ce799c 100644 --- a/apps/mobile/src/i18n/locales/kn.json +++ b/apps/mobile/src/i18n/locales/kn.json @@ -3013,6 +3013,7 @@ "chooseAccount": "ಖಾತೆಯನ್ನು ಆಯ್ಕೆಮಾಡಿ", "loadErrorTitle": "ನಿಮ್ಮ ಸಂಸ್ಥೆಗಳನ್ನು ಲೋಡ್ ಮಾಡಲು ಸಾಧ್ಯವಾಗಲಿಲ್ಲ.", + "membershipLostNotice": "ಈ ಸಂಸ್ಥೆಗೆ ನಿಮಗೆ ಇನ್ನು ಪ್ರವೇಶವಿಲ್ಲ. {{target}}ಗೆ ಬದಲಾಯಿಸಲಾಗಿದೆ.", "noOrganization": "ಯಾವುದೇ ಸಂಸ್ಥೆ ಇಲ್ಲ", "noOrganizationDescription": "ಈ ಖಾತೆಯು ಯಾವುದೇ ಸಂಸ್ಥೆಯ ಭಾಗವಲ್ಲ. ಸದಸ್ಯರ ಆಮಂತ್ರಣಗಳು ಮತ್ತು ಕಡಿಮೆ ಬ್ಯಾಲೆನ್ಸ್ ಎಚ್ಚರಿಕೆಗಳಂತಹ ಸಂಸ್ಥೆ ವೈಶಿಷ್ಟ್ಯಗಳನ್ನು ಬಳಸಲು ವೆಬ್‌ನಲ್ಲಿ ಸಂಸ್ಥೆಯನ್ನು ರಚಿಸಿ ಅಥವಾ ಸೇರಿ.", diff --git a/apps/mobile/src/i18n/locales/ko.json b/apps/mobile/src/i18n/locales/ko.json index 75d512cd9bc..0661708073f 100644 --- a/apps/mobile/src/i18n/locales/ko.json +++ b/apps/mobile/src/i18n/locales/ko.json @@ -3005,6 +3005,7 @@ "chooseAccount": "계정 선택", "loadErrorTitle": "조직 목록을 불러올 수 없습니다.", + "membershipLostNotice": "이 조직에 더 이상 접근할 수 없습니다. {{target}}(으)로 전환되었습니다.", "noOrganization": "조직 없음", "noOrganizationDescription": "이 계정은 조직에 속해 있지 않습니다. 멤버 초대 및 잔액 부족 알림과 같은 조직 기능을 사용하려면 웹에서 조직을 만들거나 참여하세요.", diff --git a/apps/mobile/src/i18n/locales/lo.json b/apps/mobile/src/i18n/locales/lo.json index 7bd36913840..07a1d2196c0 100644 --- a/apps/mobile/src/i18n/locales/lo.json +++ b/apps/mobile/src/i18n/locales/lo.json @@ -3013,6 +3013,7 @@ "chooseAccount": "ເລືອກບັນຊີ", "loadErrorTitle": "ບໍ່ສາມາດໂຫຼດລາຍຊື່ອົງກອນຂອງທ່ານໄດ້.", + "membershipLostNotice": "ທ່ານບໍ່ມີສິດເຂົ້າເຖິງອົງກອນນີ້ອີກແລ້ວ. ປ່ຽນໄປ {{target}} ແລ້ວ.", "noOrganization": "ບໍ່ມີອົງກອນ", "noOrganizationDescription": "ບັນຊີນີ້ບໍ່ແມ່ນສ່ວນໜຶ່ງຂອງອົງກອນ. ສ້າງ ຫຼື ເຂົ້າຮ່ວມອົງກອນໃນເວັບ ເພື່ອໃຊ້ຄຸນສົມບັດຂອງອົງກອນ ເຊັ່ນ ການເຊີນສະມາຊິກ ແລະ ການແຈ້ງເຕືອນຍອດເງິນຕ່ຳ.", diff --git a/apps/mobile/src/i18n/locales/lt.json b/apps/mobile/src/i18n/locales/lt.json index e5ad0b9482f..8a5d393fac8 100644 --- a/apps/mobile/src/i18n/locales/lt.json +++ b/apps/mobile/src/i18n/locales/lt.json @@ -3093,6 +3093,7 @@ "chooseAccount": "Pasirinkite paskyrą", "loadErrorTitle": "Nepavyko įkelti jūsų organizacijų.", + "membershipLostNotice": "Jūs nebeturite prieigos prie šios organizacijos. Perjungta į {{target}}.", "noOrganization": "Nėra organizacijos", "noOrganizationDescription": "Ši paskyra nepriklauso jokiai organizacijai. Kad galėtumėte naudoti organizacijos funkcijas, tokias kaip narių kvietimai ir įspėjimai apie mažą likutį, sukurkite organizaciją arba prisijunkite prie jos naršyklėje.", diff --git a/apps/mobile/src/i18n/locales/lv.json b/apps/mobile/src/i18n/locales/lv.json index 8f7699767e6..5609c7be0de 100644 --- a/apps/mobile/src/i18n/locales/lv.json +++ b/apps/mobile/src/i18n/locales/lv.json @@ -3053,6 +3053,7 @@ "chooseAccount": "Izvēlies kontu", "loadErrorTitle": "Neizdevās ielādēt tavas organizācijas.", + "membershipLostNotice": "Tev vairs nav piekļuves šai organizācijai. Pārslēgts uz {{target}}.", "noOrganization": "Nav organizācijas", "noOrganizationDescription": "Šis konts nav daļa no organizācijas. Lai izmantotu organizācijas funkcijas, piemēram, dalībnieku uzaicinājumus un brīdinājumus par zemu atlikumu, izveido vai pievienojies organizācijai tīmeklī.", diff --git a/apps/mobile/src/i18n/locales/mg.json b/apps/mobile/src/i18n/locales/mg.json index 51fe4241a1a..1075072030e 100644 --- a/apps/mobile/src/i18n/locales/mg.json +++ b/apps/mobile/src/i18n/locales/mg.json @@ -3013,6 +3013,7 @@ "chooseAccount": "Misafidiana kaonty", "loadErrorTitle": "Tsy voaray ny fikambananao.", + "membershipLostNotice": "Tsy manana alalana hiditra amin'ity fikambanana ity intsony ianao. Nafindra tany amin'ny {{target}}.", "noOrganization": "Tsy misy fikambanana", "noOrganizationDescription": "Ity kaonty ity dia tsy anisan'ny fikambanana. Mamorona na midira ao amin'ny fikambanana ao amin'ny tranonkala mba hampiasa ny endriky ny fikambanana toy ny fanasana mpikambana sy ny fampitandremana momba ny fifandanjana ambany.", diff --git a/apps/mobile/src/i18n/locales/mi.json b/apps/mobile/src/i18n/locales/mi.json index f1a4820b1f8..ca58348f701 100644 --- a/apps/mobile/src/i18n/locales/mi.json +++ b/apps/mobile/src/i18n/locales/mi.json @@ -3013,6 +3013,7 @@ "chooseAccount": "Kōwhiria tētahi pūkete", "loadErrorTitle": "Kāore i taea te uta i ō rōpū.", + "membershipLostNotice": "Kāore ō whakaaetanga uru ki tēnei rōpū anō. I whakawhiti ki {{target}}.", "noOrganization": "Kāore he rōpū", "noOrganizationDescription": "Kāore tēnei pūkete i roto i tētahi rōpū. Waihangatia, uru atu rānei ki tētahi rōpū i runga i te paetukutuku kia whakamahi ai i ngā āhuatanga rōpū pēnei i ngā pōwhiri mema me ngā whakatūpato mō te toenga iti.", diff --git a/apps/mobile/src/i18n/locales/mk.json b/apps/mobile/src/i18n/locales/mk.json index 2bc856c5b5a..6a3ea4b3b5f 100644 --- a/apps/mobile/src/i18n/locales/mk.json +++ b/apps/mobile/src/i18n/locales/mk.json @@ -3013,6 +3013,7 @@ "chooseAccount": "Избери сметка", "loadErrorTitle": "Не успеа вчитувањето на твоите организации.", + "membershipLostNotice": "Веќе немаш пристап до оваа организација. Префрлено на {{target}}.", "noOrganization": "Нема организација", "noOrganizationDescription": "Оваа сметка не е дел од организација. Создај или придружи се на организација на веб-страницата за да ги користиш функциите на организацијата, како покани за членови и предупредувања за низок баланс.", diff --git a/apps/mobile/src/i18n/locales/ml.json b/apps/mobile/src/i18n/locales/ml.json index 07e9e229719..b20af48d925 100644 --- a/apps/mobile/src/i18n/locales/ml.json +++ b/apps/mobile/src/i18n/locales/ml.json @@ -3013,6 +3013,7 @@ "chooseAccount": "ഒരു അക്കൗണ്ട് തിരഞ്ഞെടുക്കുക", "loadErrorTitle": "നിങ്ങളുടെ ഓർഗനൈസേഷനുകൾ ലോഡ് ചെയ്യാൻ കഴിഞ്ഞില്ല.", + "membershipLostNotice": "ഈ ഓർഗനൈസേഷനിൽ നിങ്ങൾക്ക് ഇനി പ്രവേശനമില്ല. {{target}}-ലേക്ക് മാറി.", "noOrganization": "ഓർഗനൈസേഷൻ ഇല്ല", "noOrganizationDescription": "ഈ അക്കൗണ്ട് ഒരു ഓർഗനൈസേഷന്റെ ഭാഗമല്ല. അംഗ ക്ഷണങ്ങളും കുറഞ്ഞ ബാലൻസ് മുന്നറിയിപ്പുകളും പോലുള്ള ഓർഗനൈസേഷൻ സവിശേഷതകൾ ഉപയോഗിക്കാൻ വെബ്സൈറ്റിൽ ഒരു ഓർഗനൈസേഷൻ സൃഷ്ടിക്കുകയോ ഒന്നിൽ ചേരുകയോ ചെയ്യുക.", diff --git a/apps/mobile/src/i18n/locales/mn.json b/apps/mobile/src/i18n/locales/mn.json index eac287257b0..838cb78b187 100644 --- a/apps/mobile/src/i18n/locales/mn.json +++ b/apps/mobile/src/i18n/locales/mn.json @@ -3013,6 +3013,7 @@ "chooseAccount": "Бүртгэл сонгох", "loadErrorTitle": "Таны байгууллагуудыг ачаалж чадсангүй.", + "membershipLostNotice": "Та энэ байгууллагад хандах эрхгүй боллоо. {{target}} руу шилжлээ.", "noOrganization": "Байгууллага байхгүй", "noOrganizationDescription": "Энэ бүртгэл нь байгууллагын гишүүн биш. Гишүүдийн урилга, бага үлдэгдлийн мэдэгдэл зэрэг байгууллагын функцуудыг ашиглахын тулд вэб сайтад байгууллага үүсгэх эсвэл нэгдээрэй.", diff --git a/apps/mobile/src/i18n/locales/mr.json b/apps/mobile/src/i18n/locales/mr.json index 68baeafe056..c80064eb1e6 100644 --- a/apps/mobile/src/i18n/locales/mr.json +++ b/apps/mobile/src/i18n/locales/mr.json @@ -3013,6 +3013,7 @@ "chooseAccount": "खाते निवडा", "loadErrorTitle": "तुमच्या संस्था लोड करता आल्या नाहीत.", + "membershipLostNotice": "तुम्हाला या संस्थेमध्ये आता प्रवेश नाही. {{target}} वर स्विच केले.", "noOrganization": "कोणतीही संस्था नाही", "noOrganizationDescription": "हे खाते कोणत्याही संस्थेचा भाग नाही. सदस्य आमंत्रणे आणि कमी शिल्लक सूचना यांसारखी संस्था वैशिष्ट्ये वापरण्यासाठी वेबवर संस्था तयार करा किंवा एकात सामील व्हा.", diff --git a/apps/mobile/src/i18n/locales/ms.json b/apps/mobile/src/i18n/locales/ms.json index f4cdb671b21..4c8f3806e58 100644 --- a/apps/mobile/src/i18n/locales/ms.json +++ b/apps/mobile/src/i18n/locales/ms.json @@ -3013,6 +3013,7 @@ "chooseAccount": "Pilih akaun", "loadErrorTitle": "Tidak dapat memuatkan organisasi anda.", + "membershipLostNotice": "Anda tidak lagi mempunyai akses kepada organisasi ini. Ditukar kepada {{target}}.", "noOrganization": "Tiada organisasi", "noOrganizationDescription": "Akaun ini bukan sebahagian daripada organisasi. Cipta atau sertai organisasi di laman web untuk menggunakan ciri organisasi seperti jemputan ahli dan amaran baki rendah.", diff --git a/apps/mobile/src/i18n/locales/mt.json b/apps/mobile/src/i18n/locales/mt.json index 594b29b6f43..c6e40497c01 100644 --- a/apps/mobile/src/i18n/locales/mt.json +++ b/apps/mobile/src/i18n/locales/mt.json @@ -3133,6 +3133,7 @@ "chooseAccount": "Agħżel kont", "loadErrorTitle": "Ma setgħux jitgħabbew l-organizzazzjonijiet tiegħek.", + "membershipLostNotice": "M'għandekx iktar aċċess għal din l-organizzazzjoni. Inbidel għal {{target}}.", "noOrganization": "L-ebda organizzazzjoni", "noOrganizationDescription": "Dan il-kont mhuwiex parti minn organizzazzjoni. Oħloq jew ingħaqad ma' organizzazzjoni fuq il-web biex tuża karatteristiċi tal-organizzazzjoni bħal stedini għall-membri u twissijiet dwar bilanċ baxx.", diff --git a/apps/mobile/src/i18n/locales/my.json b/apps/mobile/src/i18n/locales/my.json index 2e61547b46f..1622083db24 100644 --- a/apps/mobile/src/i18n/locales/my.json +++ b/apps/mobile/src/i18n/locales/my.json @@ -3013,6 +3013,7 @@ "chooseAccount": "အကောင့်တစ်ခု ရွေးပါ", "loadErrorTitle": "သင့်အဖွဲ့အစည်းများကို ရယူ၍မရပါ။", + "membershipLostNotice": "ဤအဖွဲ့အစည်းသို့ ဝင်ရောက်ခွင့် မရှိတော့ပါ။ {{target}} သို့ ပြောင်းလဲခဲ့သည်။", "noOrganization": "အဖွဲ့အစည်း မရှိပါ", "noOrganizationDescription": "ဤအကောင့်သည် အဖွဲ့အစည်းတစ်ခု၏ အစိတ်အပိုင်း မဟုတ်ပါ။ အဖွဲ့ဝင် ဖိတ်ကြားမှုများနှင့် လက်ကျန်နည်းသော အသိပေးချက်များကဲ့သို့သော အဖွဲ့အစည်း လုပ်ဆောင်ချက်များကို အသုံးပြုရန် ဝက်ဘ်ဆိုက်တွင် အဖွဲ့အစည်းတစ်ခု ဖန်တီးပါ သို့မဟုတ် ပါဝင်ပါ။", diff --git a/apps/mobile/src/i18n/locales/nb.json b/apps/mobile/src/i18n/locales/nb.json index 963851c8576..41d666f41f1 100644 --- a/apps/mobile/src/i18n/locales/nb.json +++ b/apps/mobile/src/i18n/locales/nb.json @@ -3013,6 +3013,7 @@ "chooseAccount": "Velg en konto", "loadErrorTitle": "Kunne ikke laste inn organisasjonene dine.", + "membershipLostNotice": "Du har ikke lenger tilgang til denne organisasjonen. Byttet til {{target}}.", "noOrganization": "Ingen organisasjon", "noOrganizationDescription": "Denne kontoen er ikke en del av en organisasjon. Opprett eller bli med i en organisasjon i nettleseren for å bruke organisasjonsfunksjoner som medlemsinvitasjoner og varsler om lav saldo.", diff --git a/apps/mobile/src/i18n/locales/ne.json b/apps/mobile/src/i18n/locales/ne.json index b008eed6f27..5916c5849b0 100644 --- a/apps/mobile/src/i18n/locales/ne.json +++ b/apps/mobile/src/i18n/locales/ne.json @@ -3013,6 +3013,7 @@ "chooseAccount": "खाता चयन गर्नुहोस्", "loadErrorTitle": "तपाईंका संस्थाहरू लोड गर्न सकिएन।", + "membershipLostNotice": "तपाईंसँग यो संस्थामा अब पहुँच छैन। {{target}} मा स्विच गरियो।", "noOrganization": "कुनै संस्था छैन", "noOrganizationDescription": "यो खाता कुनै संस्थाको भाग होइन। सदस्य निमन्त्रणा र कम ब्यालेन्स सतर्कता जस्ता संस्था सुविधाहरू प्रयोग गर्न वेबमा संस्था सिर्जना गर्नुहोस् वा सामेल हुनुहोस्।", diff --git a/apps/mobile/src/i18n/locales/nl.json b/apps/mobile/src/i18n/locales/nl.json index 542fe972dfc..e592f2a220b 100644 --- a/apps/mobile/src/i18n/locales/nl.json +++ b/apps/mobile/src/i18n/locales/nl.json @@ -3005,6 +3005,7 @@ "chooseAccount": "Een account kiezen", "loadErrorTitle": "Je organisaties konden niet worden geladen.", + "membershipLostNotice": "Je hebt geen toegang meer tot deze organisatie. Overgeschakeld naar {{target}}.", "noOrganization": "Geen organisatie", "noOrganizationDescription": "Dit account maakt geen deel uit van een organisatie. Maak er een aan of word lid van een organisatie op de website om organisatiefuncties zoals liduitnodigingen en waarschuwingen bij een laag saldo te gebruiken.", diff --git a/apps/mobile/src/i18n/locales/om.json b/apps/mobile/src/i18n/locales/om.json index 56394d3a8c5..7940442de74 100644 --- a/apps/mobile/src/i18n/locales/om.json +++ b/apps/mobile/src/i18n/locales/om.json @@ -3013,6 +3013,7 @@ "chooseAccount": "Herrega filadhu", "loadErrorTitle": "Dhaabbilee kee fe'uu hin danda'amne.", + "membershipLostNotice": "Dhaabbata kana bira ga'uuf kana booda hayyama hin qabdu. Gara {{target}} tti jijjiirame.", "noOrganization": "Dhaabbanni hin jiru", "noOrganizationDescription": "Herregni kun dhaabbata tokko keessaa hin qoodu. Faayidaalee dhaabbataa akka affeerraa miseensotaa fi akeekkachiisa haala maallaqaa gadii fayyadamuf marsariitii irratti dhaabbata uumi yookaan tokkotti makami.", diff --git a/apps/mobile/src/i18n/locales/or.json b/apps/mobile/src/i18n/locales/or.json index fa9b05f5dbd..da0ddbe2d07 100644 --- a/apps/mobile/src/i18n/locales/or.json +++ b/apps/mobile/src/i18n/locales/or.json @@ -3013,6 +3013,7 @@ "chooseAccount": "ଏକ ଖାତା ବାଛନ୍ତୁ", "loadErrorTitle": "ଆପଣଙ୍କ ସଂଗଠନଗୁଡ଼ିକ ଲୋଡ୍ ହୋଇପାରିଲା ନାହିଁ।", + "membershipLostNotice": "ଆପଣଙ୍କର ଏହି ସଂଗଠନରେ ଆଉ ପ୍ରବେଶ ଅଧିକାର ନାହିଁ। {{target}} କୁ ସ୍ୱିଚ୍ ହୋଇଛି।", "noOrganization": "କୋଣସି ସଂଗଠନ ନାହିଁ", "noOrganizationDescription": "ଏହି ଖାତା କୌଣସି ସଂଗଠନର ଅଂଶ ନୁହେଁ। ସଦସ୍ୟ ଆମନ୍ତ୍ରଣ ଏବଂ କମ୍ ବାଲାନ୍ସ ସତର୍କତା ପରି ସଂଗଠନ ସୁବିଧାଗୁଡ଼ିକ ବ୍ୟବହାର କରିବାକୁ ୱେବ୍‌ରେ ଏକ ସଂଗଠନ ସୃଷ୍ଟି କରନ୍ତୁ କିମ୍ବା ଯୋଗ ଦିଅନ୍ତୁ।", diff --git a/apps/mobile/src/i18n/locales/pa.json b/apps/mobile/src/i18n/locales/pa.json index 411cc3e244d..8466d3066d1 100644 --- a/apps/mobile/src/i18n/locales/pa.json +++ b/apps/mobile/src/i18n/locales/pa.json @@ -3013,6 +3013,7 @@ "chooseAccount": "ਇੱਕ ਖਾਤਾ ਚੁਣੋ", "loadErrorTitle": "ਤੁਹਾਡੀਆਂ ਸੰਸਥਾਵਾਂ ਲੋਡ ਨਹੀਂ ਹੋ ਸਕੀਆਂ।", + "membershipLostNotice": "ਤੁਹਾਡੀ ਇਸ ਸੰਸਥਾ ਤੱਕ ਹੁਣ ਪਹੁੰਚ ਨਹੀਂ ਹੈ। {{target}} ਤੇ ਸਵਿਚ ਹੋਇਆ।", "noOrganization": "ਕੋਈ ਸੰਸਥਾ ਨਹੀਂ", "noOrganizationDescription": "ਇਹ ਖਾਤਾ ਕਿਸੇ ਸੰਸਥਾ ਦਾ ਹਿੱਸਾ ਨਹੀਂ ਹੈ। ਮੈਂਬਰ ਸੱਦੇ ਅਤੇ ਘੱਟ ਬੈਲੰਸ ਚੇਤਾਵਨੀਆਂ ਵਰਗੀਆਂ ਸੰਸਥਾ ਸੁਵਿਧਾਵਾਂ ਦੀ ਵਰਤੋਂ ਕਰਨ ਲਈ ਵੈੱਬ 'ਤੇ ਸੰਸਥਾ ਬਣਾਓ ਜਾਂ ਇੱਕ ਵਿੱਚ ਸ਼ਾਮਲ ਹੋਵੋ।", diff --git a/apps/mobile/src/i18n/locales/pl.json b/apps/mobile/src/i18n/locales/pl.json index 926ec7d74a3..ab99006112d 100644 --- a/apps/mobile/src/i18n/locales/pl.json +++ b/apps/mobile/src/i18n/locales/pl.json @@ -3085,6 +3085,7 @@ "chooseAccount": "Wybierz konto", "loadErrorTitle": "Nie udało się wczytać twoich organizacji.", + "membershipLostNotice": "Nie masz już dostępu do tej organizacji. Przełączono na {{target}}.", "noOrganization": "Brak organizacji", "noOrganizationDescription": "To konto nie należy do organizacji. Utwórz organizację lub dołącz do niej w przeglądarce, aby korzystać z funkcji organizacji, takich jak zaproszenia dla członków i alerty o niskim saldzie.", diff --git a/apps/mobile/src/i18n/locales/ps.json b/apps/mobile/src/i18n/locales/ps.json index 19fb2bcf573..8081770bcf2 100644 --- a/apps/mobile/src/i18n/locales/ps.json +++ b/apps/mobile/src/i18n/locales/ps.json @@ -3013,6 +3013,7 @@ "chooseAccount": "یو حساب غوره کړئ", "loadErrorTitle": "ستاسو سازمانونه بار نه شول.", + "membershipLostNotice": "تاسو دې سازمان ته نور لاسرسی نه لرئ. په {{target}} بدل شو.", "noOrganization": "هیڅ سازمان نشته", "noOrganizationDescription": "دا حساب د یو سازمان برخه نه ده. د غړو بلنې او د ټیټ بیلانس خبرتیاوې په څیر د سازمان ځانګړتیاوې کارولو لپاره په ویب کې سازمان جوړ کړئ یا یو ځای شئ.", diff --git a/apps/mobile/src/i18n/locales/pt-BR.json b/apps/mobile/src/i18n/locales/pt-BR.json index f8a73912b92..2ad01c42f74 100644 --- a/apps/mobile/src/i18n/locales/pt-BR.json +++ b/apps/mobile/src/i18n/locales/pt-BR.json @@ -3045,6 +3045,7 @@ "chooseAccount": "Selecione uma conta", "loadErrorTitle": "Não foi possível carregar suas organizações.", + "membershipLostNotice": "Você não tem mais acesso a esta organização. Mudou para {{target}}.", "noOrganization": "Sem organização", "noOrganizationDescription": "Esta conta não faz parte de uma organização. Crie ou entre em uma organização no site para usar recursos de organização como convites de membros e alertas de saldo baixo.", diff --git a/apps/mobile/src/i18n/locales/pt.json b/apps/mobile/src/i18n/locales/pt.json index 106ea1f82e0..e8f3a5dc63f 100644 --- a/apps/mobile/src/i18n/locales/pt.json +++ b/apps/mobile/src/i18n/locales/pt.json @@ -3053,6 +3053,7 @@ "chooseAccount": "Selecionar uma conta", "loadErrorTitle": "Não foi possível carregar as suas organizações.", + "membershipLostNotice": "Já não tem acesso a esta organização. Mudou para {{target}}.", "noOrganization": "Sem organização", "noOrganizationDescription": "Esta conta não faz parte de uma organização. Crie ou ingresse em uma organização na web para usar recursos de organização como convites de membros e alertas de saldo baixo.", diff --git a/apps/mobile/src/i18n/locales/ro.json b/apps/mobile/src/i18n/locales/ro.json index 050e20efe6c..5f8d262d675 100644 --- a/apps/mobile/src/i18n/locales/ro.json +++ b/apps/mobile/src/i18n/locales/ro.json @@ -3053,6 +3053,7 @@ "chooseAccount": "Selectează un cont", "loadErrorTitle": "Nu s-au putut încărca organizațiile tale.", + "membershipLostNotice": "Nu mai ai acces la această organizație. S-a comutat la {{target}}.", "noOrganization": "Nicio organizație", "noOrganizationDescription": "Acest cont nu face parte dintr-o organizație. Creează sau alătură-te unei organizații pe site pentru a folosi funcții de organizație precum invitațiile pentru membri și alertele de sold scăzut.", diff --git a/apps/mobile/src/i18n/locales/ru.json b/apps/mobile/src/i18n/locales/ru.json index 8f06b54a0d2..a5d15bc58b7 100644 --- a/apps/mobile/src/i18n/locales/ru.json +++ b/apps/mobile/src/i18n/locales/ru.json @@ -3085,6 +3085,7 @@ "chooseAccount": "Выберите аккаунт", "loadErrorTitle": "Не удалось загрузить список ваших организаций.", + "membershipLostNotice": "У вас больше нет доступа к этой организации. Переключено на {{target}}.", "noOrganization": "Нет организации", "noOrganizationDescription": "Этот аккаунт не входит в организацию. Создайте организацию или присоединитесь к ней в веб-версии, чтобы использовать функции организации, такие как приглашения участников и оповещения о низком балансе.", diff --git a/apps/mobile/src/i18n/locales/si.json b/apps/mobile/src/i18n/locales/si.json index d23adb3ba65..73b002961ea 100644 --- a/apps/mobile/src/i18n/locales/si.json +++ b/apps/mobile/src/i18n/locales/si.json @@ -3013,6 +3013,7 @@ "chooseAccount": "ගිණුමක් තෝරන්න", "loadErrorTitle": "ඔබේ සංවිධාන පූරණය කළ නොහැකි විය.", + "membershipLostNotice": "ඔබට මෙම සංවිධානයට තවදුරටත් ප්‍රවේශ වීමට අවසර නොමැත. {{target}} වෙත මාරු විය.", "noOrganization": "සංවිධානයක් නැත", "noOrganizationDescription": "මෙම ගිණුම සංවිධානයක කොටසක් නොවේ. සාමාජික ආරාධනා සහ අඩු ශේෂ ඇඟවීම් වැනි සංවිධාන විශේෂාංග භාවිත කිරීමට වෙබ් අඩවියේ සංවිධානයක් සාදන්න හෝ එකකට එකතු වන්න.", diff --git a/apps/mobile/src/i18n/locales/sk.json b/apps/mobile/src/i18n/locales/sk.json index a2f6fae98ee..342c1da6784 100644 --- a/apps/mobile/src/i18n/locales/sk.json +++ b/apps/mobile/src/i18n/locales/sk.json @@ -3093,6 +3093,7 @@ "chooseAccount": "Vyber účet", "loadErrorTitle": "Tvoje organizácie sa nepodarilo načítať.", + "membershipLostNotice": "Už nemáš prístup k tejto organizácii. Prepnuté na {{target}}.", "noOrganization": "Žiadna organizácia", "noOrganizationDescription": "Tento účet nie je súčasťou organizácie. Vytvor organizáciu alebo sa k nej pripoj na webe, aby si mohol používať funkcie organizácie, ako sú pozvánky pre členov a upozornenia na nízky zostatok.", diff --git a/apps/mobile/src/i18n/locales/sl.json b/apps/mobile/src/i18n/locales/sl.json index 695c93d8a02..1461eb74938 100644 --- a/apps/mobile/src/i18n/locales/sl.json +++ b/apps/mobile/src/i18n/locales/sl.json @@ -3093,6 +3093,7 @@ "chooseAccount": "Izberi račun", "loadErrorTitle": "Tvojih organizacij ni bilo mogoče naložiti.", + "membershipLostNotice": "Nimaš več dostopa do te organizacije. Preklopljeno na {{target}}.", "noOrganization": "Ni organizacije", "noOrganizationDescription": "Ta račun ni del organizacije. Ustvari ali se pridruži organizaciji na spletu, da boš lahko uporabljal funkcije organizacije, kot so povabila za člane in opozorila o nizkem stanju.", diff --git a/apps/mobile/src/i18n/locales/so.json b/apps/mobile/src/i18n/locales/so.json index 8aa6d5423cb..8701f520a38 100644 --- a/apps/mobile/src/i18n/locales/so.json +++ b/apps/mobile/src/i18n/locales/so.json @@ -3013,6 +3013,7 @@ "chooseAccount": "Dooro akoon", "loadErrorTitle": "Ururradaada lama soo shubi karin.", + "membershipLostNotice": "Mar dambe ma lihid oggolaansho aad ku gasho ururkan. Waxaa lagu beddelay {{target}}.", "noOrganization": "Ma jiro urur", "noOrganizationDescription": "Akoonkani qayb kama aha urur. Samee ama ku biir urur bogga internetka si aad u isticmaasho sifooyinka ururka sida casumaadka xubnaha iyo digniinaha hadhaaga hooseeya.", diff --git a/apps/mobile/src/i18n/locales/sq.json b/apps/mobile/src/i18n/locales/sq.json index b8d7795abb6..2b0845fd939 100644 --- a/apps/mobile/src/i18n/locales/sq.json +++ b/apps/mobile/src/i18n/locales/sq.json @@ -3013,6 +3013,7 @@ "chooseAccount": "Zgjidh një llogari", "loadErrorTitle": "Nuk u ngarkuan organizatat e tua.", + "membershipLostNotice": "Nuk ke më qasje në këtë organizatë. U kalua te {{target}}.", "noOrganization": "Asnjë organizatë", "noOrganizationDescription": "Kjo llogari nuk është pjesë e një organizate. Krijo ose bashkohu me një organizatë në ueb për të përdorur veçoritë e organizatës si ftesat për anëtarët dhe alarmet për balancë të ulët.", diff --git a/apps/mobile/src/i18n/locales/sr.json b/apps/mobile/src/i18n/locales/sr.json index a4879abf44e..8f4c04b199e 100644 --- a/apps/mobile/src/i18n/locales/sr.json +++ b/apps/mobile/src/i18n/locales/sr.json @@ -3053,6 +3053,7 @@ "chooseAccount": "Izaberi nalog", "loadErrorTitle": "Učitavanje tvojih organizacija nije uspelo.", + "membershipLostNotice": "Više nemaš pristup ovoj organizaciji. Prebačeno na {{target}}.", "noOrganization": "Nema organizacije", "noOrganizationDescription": "Ovaj nalog nije deo organizacije. Napravi ili se pridruži organizaciji na vebu da bi koristio funkcije organizacije kao što su pozivnice za članove i upozorenja o niskom saldu.", diff --git a/apps/mobile/src/i18n/locales/sv.json b/apps/mobile/src/i18n/locales/sv.json index da6bd8ab1cf..48efeec078d 100644 --- a/apps/mobile/src/i18n/locales/sv.json +++ b/apps/mobile/src/i18n/locales/sv.json @@ -3013,6 +3013,7 @@ "chooseAccount": "Välj ett konto", "loadErrorTitle": "Det gick inte att läsa in dina organisationer.", + "membershipLostNotice": "Du har inte längre åtkomst till den här organisationen. Växlade till {{target}}.", "noOrganization": "Ingen organisation", "noOrganizationDescription": "Det här kontot tillhör ingen organisation. Skapa eller gå med i en organisation på webben för att använda organisationsfunktioner som medlemsinbjudningar och varningar om lågt saldo.", diff --git a/apps/mobile/src/i18n/locales/sw.json b/apps/mobile/src/i18n/locales/sw.json index 58c0f5a650d..ac51b89157c 100644 --- a/apps/mobile/src/i18n/locales/sw.json +++ b/apps/mobile/src/i18n/locales/sw.json @@ -3013,6 +3013,7 @@ "chooseAccount": "Chagua akaunti", "loadErrorTitle": "Imeshindikana kupakia mashirika yako.", + "membershipLostNotice": "Huna tena ruhusa ya kufikia shirika hili. Imebadilishwa kwenda {{target}}.", "noOrganization": "Hakuna shirika", "noOrganizationDescription": "Akaunti hii si sehemu ya shirika. Unda au jiunge na shirika kwenye wavuti ili kutumia vipengele vya shirika kama vile mialiko ya wanachama na arifa za salio la chini.", diff --git a/apps/mobile/src/i18n/locales/ta.json b/apps/mobile/src/i18n/locales/ta.json index 73a609c914b..2d8f10daa09 100644 --- a/apps/mobile/src/i18n/locales/ta.json +++ b/apps/mobile/src/i18n/locales/ta.json @@ -3013,6 +3013,7 @@ "chooseAccount": "ஒரு கணக்கைத் தேர்ந்தெடுக்கவும்", "loadErrorTitle": "உங்கள் நிறுவனங்களை ஏற்ற முடியவில்லை.", + "membershipLostNotice": "இந்த நிறுவனத்தை இனி அணுக உங்களுக்கு அனுமதி இல்லை. {{target}}-க்கு மாற்றப்பட்டது.", "noOrganization": "நிறுவனம் இல்லை", "noOrganizationDescription": "இந்தக் கணக்கு ஒரு நிறுவனத்தின் பகுதி அல்ல. உறுப்பினர் அழைப்புகள் மற்றும் குறைந்த இருப்பு எச்சரிக்கைகள் போன்ற நிறுவன அம்சங்களைப் பயன்படுத்த வலைத்தளத்தில் ஒரு நிறுவனத்தை உருவாக்கவும் அல்லது அதில் சேரவும்.", diff --git a/apps/mobile/src/i18n/locales/te.json b/apps/mobile/src/i18n/locales/te.json index e1e31408975..766a01efc3a 100644 --- a/apps/mobile/src/i18n/locales/te.json +++ b/apps/mobile/src/i18n/locales/te.json @@ -3013,6 +3013,7 @@ "chooseAccount": "ఖాతాను ఎంచుకోండి", "loadErrorTitle": "మీ సంస్థలను లోడ్ చేయలేకపోయాము.", + "membershipLostNotice": "ఈ సంస్థను ఇక యాక్సెస్ చేయడానికి మీకు అనుమతి లేదు. {{target}}కి మార్చబడింది.", "noOrganization": "సంస్థ లేదు", "noOrganizationDescription": "ఈ ఖాతా ఏ సంస్థలోనూ భాగం కాదు. సభ్యుల ఆహ్వానాలు మరియు తక్కువ బ్యాలెన్స్ హెచ్చరికల వంటి సంస్థ ఫీచర్‌లను ఉపయోగించడానికి వెబ్‌సైట్‌లో సంస్థను సృష్టించండి లేదా చేరండి.", diff --git a/apps/mobile/src/i18n/locales/th.json b/apps/mobile/src/i18n/locales/th.json index c8932ed9ac6..682f31a3370 100644 --- a/apps/mobile/src/i18n/locales/th.json +++ b/apps/mobile/src/i18n/locales/th.json @@ -3013,6 +3013,7 @@ "chooseAccount": "เลือกบัญชี", "loadErrorTitle": "โหลดองค์กรของคุณไม่สำเร็จ", + "membershipLostNotice": "คุณไม่มีสิทธิ์เข้าถึงองค์กรนี้อีกต่อไป เปลี่ยนเป็น {{target}} แล้ว", "noOrganization": "ไม่มีองค์กร", "noOrganizationDescription": "บัญชีนี้ไม่ได้เป็นส่วนหนึ่งขององค์กร สร้างหรือเข้าร่วมองค์กรบนเว็บเพื่อใช้ฟีเจอร์ขององค์กร เช่น คำเชิญสมาชิกและการแจ้งเตือนยอดคงเหลือต่ำ", diff --git a/apps/mobile/src/i18n/locales/tr.json b/apps/mobile/src/i18n/locales/tr.json index be9190cb2a9..b0d79155ed6 100644 --- a/apps/mobile/src/i18n/locales/tr.json +++ b/apps/mobile/src/i18n/locales/tr.json @@ -3005,6 +3005,7 @@ "chooseAccount": "Bir hesap seç", "loadErrorTitle": "Kuruluşların yüklenemedi.", + "membershipLostNotice": "Bu kuruluşa artık erişimin yok. {{target}}'e geçildi.", "noOrganization": "Kuruluş yok", "noOrganizationDescription": "Bu hesap bir kuruluşun parçası değil. Üye davetleri ve düşük bakiye uyarıları gibi kuruluş özelliklerini kullanmak için web sitesinde bir kuruluş oluştur veya birine katıl.", diff --git a/apps/mobile/src/i18n/locales/uk.json b/apps/mobile/src/i18n/locales/uk.json index 2e8d49e36fc..d745f1ad8d9 100644 --- a/apps/mobile/src/i18n/locales/uk.json +++ b/apps/mobile/src/i18n/locales/uk.json @@ -3085,6 +3085,7 @@ "chooseAccount": "Виберіть обліковий запис", "loadErrorTitle": "Не вдалося завантажити ваші організації.", + "membershipLostNotice": "У вас більше немає доступу до цієї організації. Перемкнуто на {{target}}.", "noOrganization": "Немає організації", "noOrganizationDescription": "Цей обліковий запис не входить до організації. Створіть організацію або приєднайтеся до неї у вебверсії, щоб використовувати функції організації, як-от запрошення учасників і сповіщення про низький баланс.", diff --git a/apps/mobile/src/i18n/locales/ur.json b/apps/mobile/src/i18n/locales/ur.json index 70ea07bacb3..9ed6dd46b88 100644 --- a/apps/mobile/src/i18n/locales/ur.json +++ b/apps/mobile/src/i18n/locales/ur.json @@ -3013,6 +3013,7 @@ "chooseAccount": "ایک اکاؤنٹ منتخب کریں", "loadErrorTitle": "آپ کی تنظیمیں لوڈ نہیں ہو سکیں۔", + "membershipLostNotice": "آپ کو اب اس تنظیم تک رسائی حاصل نہیں ہے۔ {{target}} پر منتقل کر دیا گیا۔", "noOrganization": "کوئی تنظیم نہیں", "noOrganizationDescription": "یہ اکاؤنٹ کسی تنظیم کا حصہ نہیں ہے۔ ممبر دعوتیں اور کم بیلنس انتباہات جیسی تنظیمی خصوصیات استعمال کرنے کے لیے ویب پر ایک تنظیم بنائیں یا اس میں شامل ہوں۔", diff --git a/apps/mobile/src/i18n/locales/uz.json b/apps/mobile/src/i18n/locales/uz.json index 294dda83e00..872c67cec49 100644 --- a/apps/mobile/src/i18n/locales/uz.json +++ b/apps/mobile/src/i18n/locales/uz.json @@ -3013,6 +3013,7 @@ "chooseAccount": "Hisobni tanlang", "loadErrorTitle": "Tashkilotlaringizni yuklab bo'lmadi.", + "membershipLostNotice": "Sizda ushbu tashkilotga kirish huquqi endi yo'q. {{target}} ga o'tkazildi.", "noOrganization": "Tashkilot yo'q", "noOrganizationDescription": "Bu hisob hech qanday tashkilotga tegishli emas. A'zolarni taklif qilish va kam balans haqidagi ogohlantirishlar kabi tashkilot funksiyalaridan foydalanish uchun saytda tashkilot yarating yoki unga qo'shiling.", diff --git a/apps/mobile/src/i18n/locales/vi.json b/apps/mobile/src/i18n/locales/vi.json index 6292038fe05..3868081dd45 100644 --- a/apps/mobile/src/i18n/locales/vi.json +++ b/apps/mobile/src/i18n/locales/vi.json @@ -3005,6 +3005,7 @@ "chooseAccount": "Chọn tài khoản", "loadErrorTitle": "Không thể tải danh sách tổ chức của bạn.", + "membershipLostNotice": "Bạn không còn quyền truy cập vào tổ chức này. Đã chuyển sang {{target}}.", "noOrganization": "Không có tổ chức", "noOrganizationDescription": "Tài khoản này không thuộc tổ chức nào. Hãy tạo hoặc tham gia một tổ chức trên web để sử dụng các tính năng của tổ chức như lời mời thành viên và cảnh báo số dư thấp.", diff --git a/apps/mobile/src/i18n/locales/yo.json b/apps/mobile/src/i18n/locales/yo.json index 2930b5ed7dc..8b77c9a8bf8 100644 --- a/apps/mobile/src/i18n/locales/yo.json +++ b/apps/mobile/src/i18n/locales/yo.json @@ -3013,6 +3013,7 @@ "chooseAccount": "Yan àkọọ́lẹ̀ kan", "loadErrorTitle": "A kò lè gbé àwọn àjọ rẹ wọlé.", + "membershipLostNotice": "O kò ní àṣẹ láti wọ àjọ yìí mọ́. A ti yí padà sí {{target}}.", "noOrganization": "Kò sí àjọ", "noOrganizationDescription": "Àkọọ́lẹ̀ yìí kì í ṣe apá kan àjọ. Ṣẹ̀dá tàbí dara pọ̀ mọ́ àjọ kan lórí wẹ́ẹ̀bù láti lo àwọn ẹ̀yà àjọ bíi ìpè àwọn ọmọ ẹgbẹ́ àti ìkìlọ̀ nípa ìyọkù tó kéré.", diff --git a/apps/mobile/src/i18n/locales/zh-Hans.json b/apps/mobile/src/i18n/locales/zh-Hans.json index e2be8b19500..9f73df6a265 100644 --- a/apps/mobile/src/i18n/locales/zh-Hans.json +++ b/apps/mobile/src/i18n/locales/zh-Hans.json @@ -3005,6 +3005,7 @@ "chooseAccount": "选择账户", "loadErrorTitle": "无法加载你的组织列表。", + "membershipLostNotice": "你已无法访问此组织。已切换至 {{target}}。", "noOrganization": "无组织", "noOrganizationDescription": "此账户不属于任何组织。请在网页版创建或加入组织,以使用成员邀请和低余额提醒等组织功能。", diff --git a/apps/mobile/src/i18n/locales/zh-Hant.json b/apps/mobile/src/i18n/locales/zh-Hant.json index cca6226c0f5..8431d662936 100644 --- a/apps/mobile/src/i18n/locales/zh-Hant.json +++ b/apps/mobile/src/i18n/locales/zh-Hant.json @@ -3005,6 +3005,7 @@ "chooseAccount": "選取帳戶", "loadErrorTitle": "無法載入你的組織清單。", + "membershipLostNotice": "你已無法存取此組織。已切換至 {{target}}。", "noOrganization": "沒有組織", "noOrganizationDescription": "此帳戶不屬於任何組織。請在網頁版建立或加入組織,以使用成員邀請和低餘額提醒等組織功能。", diff --git a/apps/mobile/src/i18n/locales/zu.json b/apps/mobile/src/i18n/locales/zu.json index 02fd5233827..944a706c38c 100644 --- a/apps/mobile/src/i18n/locales/zu.json +++ b/apps/mobile/src/i18n/locales/zu.json @@ -3013,6 +3013,7 @@ "chooseAccount": "Khetha i-akhawunti", "loadErrorTitle": "Asikwazanga ukulayisha izinhlangano zakho.", + "membershipLostNotice": "Awusenayo imvume yokufinyelela kule nhlangano. Ushintshelwe ku-{{target}}.", "noOrganization": "Ayikho inhlangano", "noOrganizationDescription": "Le akhawunti ayiyona ingxenye yenhlangano. Dala noma ujoyine inhlangano kuwebhu ukuze usebenzise izici zenhlangano njengezimemo zamalungu nezexwayiso zebhalansi ephansi.", diff --git a/apps/mobile/src/lib/deep-link-launch.mounted.test.tsx b/apps/mobile/src/lib/deep-link-launch.mounted.test.tsx index e85bddd8961..fd9e42136d0 100644 --- a/apps/mobile/src/lib/deep-link-launch.mounted.test.tsx +++ b/apps/mobile/src/lib/deep-link-launch.mounted.test.tsx @@ -19,6 +19,14 @@ import { const auth = vi.hoisted(() => ({ token: 't' as string | undefined, isLoading: false })); const navigate = vi.hoisted(() => vi.fn()); +// The provider now announces the lost-membership recovery through the +// toast adapter, whose module graph reaches react-native's Flow source +// the mounted transform cannot parse. Suites mock the adapter (not the +// sonner package) so the mock sits at the edge that imports it. +vi.mock('@/lib/a11y/announcing-toast', () => ({ + announcingToast: { success: vi.fn(), error: vi.fn(), warning: vi.fn() }, +})); + vi.mock('@sentry/react-native', () => ({ captureException: vi.fn() })); vi.mock('@/lib/auth/auth-context', () => ({ useAuth: () => auth })); vi.mock('@/lib/auth/logout-cleanup', () => ({ unregisterActivityTokensAndTombstone: vi.fn() })); @@ -31,7 +39,13 @@ vi.mock('@/lib/trpc', () => ({ list: { queryOptions: () => ({ queryKey: ['organizations', 'list'], - queryFn: () => [], + // The seeded selection must stay in the fetched list, or the + // provider's lost-membership recovery switches it away before the + // deep link under test runs. + queryFn: () => [ + { organizationId: 'org-1', organizationName: 'Org 1', role: 'owner' }, + { organizationId: 'org-2', organizationName: 'Org 2', role: 'owner' }, + ], }), }, }, diff --git a/apps/mobile/src/lib/organization-context.mounted.test.tsx b/apps/mobile/src/lib/organization-context.mounted.test.tsx index 4102279c756..3e1117dde57 100644 --- a/apps/mobile/src/lib/organization-context.mounted.test.tsx +++ b/apps/mobile/src/lib/organization-context.mounted.test.tsx @@ -22,6 +22,14 @@ const auth = vi.hoisted(() => ({ token: 'token-a' as string | undefined })); const storage = vi.hoisted(() => ({ read: vi.fn(), write: vi.fn(), remove: vi.fn() })); const list = vi.hoisted(() => vi.fn()); +// The provider now announces the lost-membership recovery through the +// toast adapter, whose module graph reaches react-native's Flow source +// the mounted transform cannot parse. Suites mock the adapter (not the +// sonner package) so the mock sits at the edge that imports it. +vi.mock('@/lib/a11y/announcing-toast', () => ({ + announcingToast: { success: vi.fn(), error: vi.fn(), warning: vi.fn() }, +})); + vi.mock('@/lib/auth/auth-context', () => ({ useAuth: () => auth })); vi.mock('@/lib/auth/logout-cleanup', () => ({ unregisterActivityTokensAndTombstone: vi.fn() })); vi.mock('expo-secure-store', () => ({ @@ -116,7 +124,10 @@ beforeEach(() => { savedMetadata.delete(key); await Promise.resolve(); }); - list.mockReset().mockResolvedValue([]); + // The default list holds both fixture organizations: a stored selection + // missing from the fetched list now recovers to the first membership, so + // the fencing tests below stage their own list payloads explicitly. + list.mockReset().mockResolvedValue([orgA, orgB]); onlineManager.setOnline(true); }); @@ -135,7 +146,7 @@ describe('OrganizationProvider default organization', () => { await mount(); expect(scope()).toMatchObject({ organizationId: 'org-b', isLoaded: true, error: null }); await act(() => { - names.resolve([orgA]); + names.resolve([orgA, orgB]); }); expect(scope().organizationId).toBe('org-b'); }); @@ -307,7 +318,8 @@ describe('OrganizationProvider restoration fencing', () => { } return savedMetadata.get(key) ?? null; }); - list.mockResolvedValue([orgA]); + // The fetched list holds the saved organization, so the selection survives. + list.mockResolvedValue([orgA, orgB]); await mount(); await waitFor(() => scope().isLoaded); diff --git a/apps/mobile/src/lib/organization-context.test.ts b/apps/mobile/src/lib/organization-context.test.ts index 76cd4220261..5c6e84b7899 100644 --- a/apps/mobile/src/lib/organization-context.test.ts +++ b/apps/mobile/src/lib/organization-context.test.ts @@ -1,11 +1,15 @@ /* oxlint-disable @typescript-eslint/no-unsafe-call @typescript-eslint/no-unsafe-member-access */ +import { type QueryClient } from '@tanstack/react-query'; import { createElement } from 'react'; import { act } from '@/test/renderer'; +import { renderWithProviders, waitFor } from '@/test/render-with-providers'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { OrganizationProvider, useOrganization } from './organization-context'; import { ORGANIZATION_PERSONAL_STORAGE_KEY, ORGANIZATION_STORAGE_KEY } from '@/lib/storage-keys'; -import { renderWithProviders } from '@/test/render-with-providers'; +// The recovery toast resolves catalog copy; the suite needs the real i18n +// singleton so the asserted strings are the English catalog's, not key names. +import '@/i18n'; const hoisted = vi.hoisted(() => ({ useAuth: vi.fn(), @@ -15,12 +19,17 @@ const hoisted = vi.hoisted(() => ({ unregisterActivityTokensAndTombstone: vi.fn(), getItemAsync: vi.fn(), list: vi.fn(), + toastWarning: vi.fn(), })); vi.mock('@/lib/auth/auth-context', () => ({ useAuth: hoisted.useAuth, })); +vi.mock('@/lib/a11y/announcing-toast', () => ({ + announcingToast: { success: vi.fn(), error: vi.fn(), warning: hoisted.toastWarning }, +})); + vi.mock('@/lib/auth/account-metadata-write', () => ({ setAccountMetadata: hoisted.setAccountMetadata, deleteAccountMetadata: hoisted.deleteAccountMetadata, @@ -58,6 +67,7 @@ type OrganizationContextValue = { async function mountProvider(): Promise<{ getCtx: () => OrganizationContextValue; + queryClient: QueryClient; unmount: () => void; }> { let capturedCtx: OrganizationContextValue | undefined = undefined; @@ -88,6 +98,7 @@ async function mountProvider(): Promise<{ } return capturedCtx; }, + queryClient: ui.queryClient, unmount: ui.unmount, }; } @@ -179,3 +190,158 @@ describe('OrganizationProvider.setOrganizationId', () => { unmount(); }); }); + +describe('OrganizationProvider lost-membership recovery', () => { + /** Stores the recovered fixture selection; every other key reads absent. */ + const storeSelection = (): void => { + hoisted.getItemAsync.mockImplementation(async key => { + const stored = key === ORGANIZATION_STORAGE_KEY ? 'org-lost' : null; + await Promise.resolve(); + return stored; + }); + }; + + beforeEach(() => { + vi.clearAllMocks(); + hoisted.useAuth.mockReturnValue({ token: 't' }); + hoisted.setAccountMetadata.mockResolvedValue(undefined); + hoisted.deleteAccountMetadata.mockResolvedValue(undefined); + hoisted.unregisterActivityTokensAndTombstone.mockResolvedValue(undefined); + }); + + /** Swaps the list payload and refetches the shared query inside act. */ + async function refetchListWith( + queryClient: QueryClient, + next: { organizationId: string; organizationName: string; role: string }[] + ): Promise { + hoisted.list.mockResolvedValue(next); + await act(async () => { + await queryClient.refetchQueries({ queryKey: ['organizations-list'] }); + }); + } + + it('switches to the first remaining membership and announces the target when the active org drops out of the list', async () => { + // The stored selection names an organization the list still holds. + storeSelection(); + hoisted.list.mockResolvedValue([ + { organizationId: 'org-lost', organizationName: 'Doomed org', role: 'member' }, + { organizationId: 'org-kept', organizationName: 'Kept org', role: 'member' }, + ]); + const { getCtx, queryClient, unmount } = await mountProvider(); + expect(getCtx().organizationId).toBe('org-lost'); + expect(hoisted.toastWarning).not.toHaveBeenCalled(); + + // The refreshed list no longer names the selection. + await refetchListWith(queryClient, [ + { organizationId: 'org-kept', organizationName: 'Kept org', role: 'member' }, + ]); + + // The switch publishes through the effect chain; the notification flush + // can land a tick after the refetch resolves. + await waitFor(() => getCtx().organizationId === 'org-kept'); + expect(getCtx().organizationId).toBe('org-kept'); + expect(hoisted.setAccountMetadata).toHaveBeenCalledWith(ORGANIZATION_STORAGE_KEY, 'org-kept'); + expect(hoisted.unregisterActivityTokensAndTombstone).toHaveBeenCalledTimes(1); + expect(hoisted.toastWarning).toHaveBeenCalledTimes(1); + expect(hoisted.toastWarning).toHaveBeenCalledWith( + 'You no longer have access to this organization. Switched to Kept org.' + ); + + unmount(); + }); + + it('falls back to Personal, writes the marker, and names Personal when no membership remains', async () => { + storeSelection(); + hoisted.list.mockResolvedValue([ + { organizationId: 'org-lost', organizationName: 'Doomed org', role: 'member' }, + ]); + const { getCtx, queryClient, unmount } = await mountProvider(); + + await refetchListWith(queryClient, []); + + await waitFor(() => getCtx().organizationId === null); + expect(getCtx().organizationId).toBeNull(); + expect(hoisted.deleteAccountMetadata).toHaveBeenCalledWith(ORGANIZATION_STORAGE_KEY); + expect(hoisted.setAccountMetadata).toHaveBeenCalledWith( + ORGANIZATION_PERSONAL_STORAGE_KEY, + 'personal' + ); + expect(hoisted.toastWarning).toHaveBeenCalledWith( + 'You no longer have access to this organization. Switched to Personal.' + ); + + unmount(); + }); + + it('keeps the selection while the list is loading, failing, or still names the org', async () => { + storeSelection(); + const pending = Promise.withResolvers<{ organizationId: string }[]>(); + hoisted.list.mockReturnValue(pending.promise); + const { getCtx, queryClient, unmount } = await mountProvider(); + expect(getCtx().organizationId).toBe('org-lost'); + + // A failed list decides nothing: the selection stays, no toast. + await act(async () => { + pending.reject(new Error('offline')); + await Promise.resolve(); + }); + expect(getCtx().organizationId).toBe('org-lost'); + expect(hoisted.toastWarning).not.toHaveBeenCalled(); + + // A refetch that still names the org changes nothing either. + await refetchListWith(queryClient, [ + { organizationId: 'org-lost', organizationName: 'Doomed org', role: 'member' }, + ]); + expect(getCtx().organizationId).toBe('org-lost'); + expect(hoisted.toastWarning).not.toHaveBeenCalled(); + expect(hoisted.setAccountMetadata).not.toHaveBeenCalled(); + + unmount(); + }); + + it('recovers a stored selection whose very first snapshot already misses it (cold start without read cache)', async () => { + // The stored selection names an organization no longer in the list: the + // persisted read cache expired, so the first live snapshot is the baseline. + storeSelection(); + hoisted.list.mockResolvedValue([ + { organizationId: 'org-kept', organizationName: 'Kept org', role: 'member' }, + ]); + const { getCtx, unmount } = await mountProvider(); + + // The first settled snapshot arms and switches without a prior good list. + await waitFor(() => getCtx().organizationId === 'org-kept'); + expect(hoisted.setAccountMetadata).toHaveBeenCalledWith(ORGANIZATION_STORAGE_KEY, 'org-kept'); + expect(hoisted.toastWarning).toHaveBeenCalledWith( + 'You no longer have access to this organization. Switched to Kept org.' + ); + + unmount(); + }); + + it('does not re-switch when the user picks another context during recovery', async () => { + storeSelection(); + hoisted.list.mockResolvedValue([ + { organizationId: 'org-lost', organizationName: 'Doomed org', role: 'member' }, + { organizationId: 'org-kept', organizationName: 'Kept org', role: 'member' }, + ]); + const { getCtx, queryClient, unmount } = await mountProvider(); + + await refetchListWith(queryClient, [ + { organizationId: 'org-kept', organizationName: 'Kept org', role: 'member' }, + ]); + await waitFor(() => getCtx().organizationId === 'org-kept'); + expect(getCtx().organizationId).toBe('org-kept'); + const switchCalls = hoisted.setAccountMetadata.mock.calls.length; + + // A manual selection over the recovered context is a plain user switch, + // not a second recovery: no extra toast, no echo of the lost org. + await act(() => { + getCtx().setOrganizationId(null); + }); + expect(getCtx().organizationId).toBeNull(); + expect(hoisted.toastWarning).toHaveBeenCalledTimes(1); + expect(hoisted.setAccountMetadata.mock.calls.length).toBe(switchCalls + 1); + + unmount(); + }); +}); diff --git a/apps/mobile/src/lib/organization-context.tsx b/apps/mobile/src/lib/organization-context.tsx index e66f02711fd..185c7b519eb 100644 --- a/apps/mobile/src/lib/organization-context.tsx +++ b/apps/mobile/src/lib/organization-context.tsx @@ -1,3 +1,7 @@ +/* eslint-disable max-lines -- the selection lifecycle (restore, default + resolution, lost-membership recovery, and the storage contract) shares one + state machine; splitting it would scatter the generation fencing that keeps + the publisher correct */ import * as SecureStore from '@/lib/auth/secure-store'; import { createContext, @@ -16,6 +20,9 @@ import { currentAuthEpoch, isCurrentAuthEpoch } from '@/lib/auth/auth-epoch'; import { unregisterActivityTokensAndTombstone } from '@/lib/auth/logout-cleanup'; import { writePrivacySnapshotAndEnd } from '@/lib/glanceable/cleanup'; import { useOrganizationsList } from '@/lib/hooks/use-organizations-list'; +import { type OrgListEntry } from '@/lib/hooks/use-organization-queries'; +import { announcingToast } from '@/lib/a11y/announcing-toast'; +import { i18n } from '@/i18n'; import { ORGANIZATION_PERSONAL_STORAGE_KEY, ORGANIZATION_STORAGE_KEY } from '@/lib/storage-keys'; /** @@ -76,6 +83,16 @@ export function OrganizationProvider({ children }: { readonly children: ReactNod // stored). An auto-resolved Personal — from an empty list or a failed restore // — is not explicit, so selecting Personal must still write the marker. const personalExplicit = useRef(false); + // Set while the provider switches away from a membership the fetched list no + // longer contains. The recovery effect owns the switch; the toast copy names + // the target context, so the target is carried beside the flag. + const recoveringRef = useRef(undefined); + // The list snapshot the current selection was published against (undefined + // until a settled list is seen for it). Detection compares only across + // snapshots: a selection published against the list it came from is valid by + // construction; only a NEW snapshot (a refetch) missing the selection means + // the membership vanished server-side. + const selectionListSeen = useRef(undefined); const restore = useCallback(async () => { generation.current += 1; @@ -83,6 +100,8 @@ export function OrganizationProvider({ children }: { readonly children: ReactNod const epoch = currentAuthEpoch(); awaitingDefault.current = false; defaultFence.current = null; + recoveringRef.current = undefined; + selectionListSeen.current = undefined; setState(current => ({ ...current, token, isLoaded: false, isSaving: false, error: null })); try { // A stored organization is an explicit choice; the marker beside it is an @@ -207,6 +226,44 @@ export function OrganizationProvider({ children }: { readonly children: ReactNod persist, ]); + // Detection: a settled, successful snapshot that does not name the stored + // organization arms the recovery effect (declared below `setOrganizationId`, + // which it calls). A restored selection has no baseline of its own, so its + // very first snapshot is evaluated: a cold start whose fetched list already + // omits the stored organization switches instead of rendering the dead + // selection with a generic credits name and a failed balance. A snapshot + // that names the selection records the baseline; from there, any later + // snapshot missing it means the membership vanished while the app ran. + // Personal (null) and the default resolution (above) need no detection. The + // baseline is recorded only after the guards pass, so a snapshot observed + // during an error is re-evaluated once the error clears. + useEffect(() => { + if (!state.isLoaded || !organizationsFetched || organizationsError) { + selectionListSeen.current = undefined; + return; + } + if (organizationsFetching || state.error !== null || state.organizationId === null) { + return; + } + const snapshot = organizations ?? []; + if (snapshot === selectionListSeen.current) { + return; + } + if (snapshot.some(entry => entry.organizationId === state.organizationId)) { + selectionListSeen.current = snapshot; + return; + } + recoveringRef.current = snapshot[0]?.organizationId ?? null; + }, [ + state.isLoaded, + state.error, + state.organizationId, + organizations, + organizationsFetched, + organizationsFetching, + organizationsError, + ]); + // This provider stays mounted above the auth gate. Reset on sign-out and // invalidate obsolete reads/saves on token changes and unmount. useEffect(() => { @@ -218,6 +275,8 @@ export function OrganizationProvider({ children }: { readonly children: ReactNod awaitingDefault.current = false; defaultFence.current = null; personalExplicit.current = false; + recoveringRef.current = undefined; + selectionListSeen.current = undefined; setState({ token, organizationId: null, isLoaded: true, isSaving: false, error: null }); } return () => { @@ -227,6 +286,11 @@ export function OrganizationProvider({ children }: { readonly children: ReactNod const setOrganizationId = useCallback( (id: string | null) => { + // A manual selection disarms the recovery switch: the user moved first, + // and the snapshot it was made against becomes the selection's baseline + // (the picker only offers organizations from that snapshot). + recoveringRef.current = undefined; + selectionListSeen.current = organizations; // An explicit selection always settles a pending default, even when it // matches the placeholder published while waiting; otherwise the default // would publish later and override the user's choice. @@ -265,9 +329,53 @@ export function OrganizationProvider({ children }: { readonly children: ReactNod })); void persist(id); }, - [token, persist, state.organizationId] + [token, persist, state.organizationId, organizations] ); + // A stored selection can go stale server-side: the host removes the + // membership (or deletes the organization) while the app still holds the id. + // Once a settled, successful list misses the selection, switch to the same + // target the default resolution would pick — the first remaining membership, + // else Personal — through the existing selection path, and say so. + useEffect(() => { + if (recoveringRef.current === undefined) { + // No stale selection detected; `setOrganizationId` owns the toast. + return; + } + // A failed or still-fetching list cannot decide the target; the stale + // selection stays armed for the next settled, successful list. + if ( + state.error !== null || + !organizationsFetched || + organizationsFetching || + organizationsError + ) { + return; + } + const target = recoveringRef.current; + recoveringRef.current = undefined; + if (target !== state.organizationId) { + setOrganizationId(target); + announcingToast.warning( + i18n.t('organization.boundary.membershipLostNotice', { + target: + target === null + ? i18n.t('common.personal') + : (organizations?.find(entry => entry.organizationId === target)?.organizationName ?? + i18n.t('common.organization')), + }) + ); + } + }, [ + state.organizationId, + state.error, + organizations, + organizationsFetched, + organizationsFetching, + organizationsError, + setOrganizationId, + ]); + // `restore` re-reads the keys, but a default that failed on the shared list // would be republished from the same failed query. Refresh the list first so // Retry re-resolves the default instead of echoing the error. diff --git a/apps/mobile/src/lib/query-client.test.ts b/apps/mobile/src/lib/query-client.test.ts index c59d539f4a7..55805c115f7 100644 --- a/apps/mobile/src/lib/query-client.test.ts +++ b/apps/mobile/src/lib/query-client.test.ts @@ -204,6 +204,91 @@ describe('permission-denied query removal', () => { }); }); +const LIST_KEY: readonly unknown[] = [['organizations', 'list'], { type: 'query' }]; +const makeOrgKey = (id: string): readonly unknown[] => [ + ['organizations', 'withMembers'], + { type: 'query', input: { organizationId: id } }, +]; +const forbidden = () => + Object.assign(new Error('forbidden'), { + data: { code: 'FORBIDDEN', httpStatus: 403 }, + }); + +describe('organizations.list refresh on permission loss', () => { + it('refetches the shared list once an org-scoped FORBIDDEN lands', async () => { + const queryClient = createKiloAppQueryClient(); + const listFn = vi.fn<() => Promise<{ organizationId: string }[]>>(); + queryClient.setQueryData(LIST_KEY, [{ organizationId: 'org-1' }]); + + const observer = new QueryObserver(queryClient, { + queryKey: makeOrgKey('org-1'), + queryFn: () => { + throw forbidden(); + }, + retry: false, + staleTime: Infinity, + }); + const unsubscribe = observer.subscribe(() => undefined); + await observer.refetch().catch(() => undefined); + unsubscribe(); + + // The denial scheduled the shared list's refresh: the fresh payload is now + // marked stale, so the next observer mount refetches it. + expect(queryClient.getQueryState(LIST_KEY)?.isInvalidated).toBe(true); + + const pending = Promise.withResolvers<{ organizationId: string }[]>(); + let gated = false; + listFn.mockImplementation(async () => { + // The initial mount fires one refetch (the invalidation marked the query + // stale); gate it so the resolution order is observable. Any further + // call answers immediately. + if (gated) { + await Promise.resolve(); + return []; + } + gated = true; + const settled = await pending.promise; + return settled; + }); + const listObserver = new QueryObserver(queryClient, { + queryKey: LIST_KEY, + queryFn: listFn, + retry: false, + staleTime: Infinity, + }); + const unsubList = listObserver.subscribe(() => undefined); + await flushQueryUpdates(); + // The mount's refetch arrived at the gated queryFn. + expect(listFn).toHaveBeenCalledTimes(1); + pending.resolve([{ organizationId: 'org-2' }]); + await flushQueryUpdates(); + expect(listObserver.getCurrentResult().data).toEqual([{ organizationId: 'org-2' }]); + unsubList(); + }); + + it('does not refresh the list for a non-org FORBIDDEN', async () => { + const queryClient = createKiloAppQueryClient(); + queryClient.setQueryData(LIST_KEY, [{ organizationId: 'org-1' }]); + + const error = forbidden(); + await expect( + queryClient.fetchQuery({ + queryKey: [['user', 'getMe'], { type: 'query' }], + staleTime: 0, + queryFn: () => { + throw error; + }, + }) + ).rejects.toBe(error); + + // No observer was attached to the list, no invalidation was scheduled: the + // cached payload stays fresh and unmarked, so a later mount refetches + // nothing. + expect(queryClient.getQueryState(LIST_KEY)?.dataUpdatedAt).toBeGreaterThan(0); + expect(queryClient.getQueryState(LIST_KEY)?.isInvalidated).toBe(false); + }); +}); + const emptyResult = { sessions: [] }; describe('accepted active-session outcomes', () => { diff --git a/apps/mobile/src/lib/query-client.ts b/apps/mobile/src/lib/query-client.ts index dd063aebc6a..3ec26bf9911 100644 --- a/apps/mobile/src/lib/query-client.ts +++ b/apps/mobile/src/lib/query-client.ts @@ -220,6 +220,36 @@ function removePermissionDeniedQueries( queryClient.removeQueries({ queryKey: query.queryKey }); } +/** + * A permission denial on an org-scoped query can mean the membership itself is + * gone (the host removed the member) while the cached `organizations.list` + * still names the org — the provider's reconciliation then has nothing to act + * on. Invalidate the shared list so it refetches despite its freshness window; + * when the fresh list drops the org, `OrganizationProvider` switches to the + * remaining context. A list still holding the org (role-scoped denial, org + * intact) changes nothing: the fresh snapshot matches the selection's own. + */ +function refreshOrganizationsListOnPermissionLoss( + queryClient: QueryClient, + error: unknown, + query: Query +): void { + if (!isPermissionDeniedError(error)) { + return; + } + const organizationId = queryKeyInput(query.queryKey)?.organizationId; + if (organizationId === undefined || organizationId === null) { + return; + } + // `cancelRefetch: false` keeps one in-flight list fetch alive when several + // org-scoped queries fail around the same time, instead of cancelling and + // restarting the shared fetch once per denial. + void queryClient.invalidateQueries( + { queryKey: [['organizations', 'list']] }, + { cancelRefetch: false } + ); +} + /** * Default freshness for app queries. Screens paint persisted-cache rows before * the network answers (U4); this keeps a mount or focus regain from re-requesting @@ -260,6 +290,7 @@ export function createKiloAppQueryClient(): QueryClient { if (!query.isActive()) { removePermissionDeniedQueries(queryClient, error, query); } + refreshOrganizationsListOnPermissionLoss(queryClient, error, query); }, }), mutationCache: new MutationCache({