From 661bb189c8203de8d3a5814ea916e336e12dcbd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= <4062236+iscekic@users.noreply.github.com> Date: Mon, 19 Oct 2026 10:10:53 +0000 Subject: [PATCH 1/9] fix(mobile): switch to a remaining context when the active membership disappears A stored organization selection can outlive the membership itself: the host removes the member (or the organization is deleted) while the app still holds the id. Home then stays on the dead organization with an access-denied live section, and the profile credits row loses its organization name and balance. OrganizationProvider now compares each settled organizations.list snapshot against the one the current selection was published with. A new snapshot missing the stored selection switches to the first remaining membership (or Personal, with the explicit marker) through the existing selection path and announces the new context with a localized toast. Because the cached list can still name the org after the server already dropped the membership, a permission denial on an org-scoped query now also refetches the shared list once, so the provider sees the change instead of waiting for the next foreground freshness pass. --- apps/mobile/src/i18n/locales/en.json | 1 + .../src/lib/organization-context.test.ts | 146 ++++++++++++++++++ apps/mobile/src/lib/organization-context.tsx | 106 +++++++++++++ apps/mobile/src/lib/query-client.test.ts | 71 +++++++++ apps/mobile/src/lib/query-client.ts | 29 ++++ 5 files changed, 353 insertions(+) diff --git a/apps/mobile/src/i18n/locales/en.json b/apps/mobile/src/i18n/locales/en.json index bfee02a658f..483a038c628 100644 --- a/apps/mobile/src/i18n/locales/en.json +++ b/apps/mobile/src/i18n/locales/en.json @@ -2989,6 +2989,7 @@ "backToProfile": "Back to profile", "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}}.", "organizationUnavailable": "Organization unavailable", "selectDescription": "Choose an organization from your profile to continue.", "selectOrganization": "Select an organization", diff --git a/apps/mobile/src/lib/organization-context.test.ts b/apps/mobile/src/lib/organization-context.test.ts index 76cd4220261..cc7e0b7151f 100644 --- a/apps/mobile/src/lib/organization-context.test.ts +++ b/apps/mobile/src/lib/organization-context.test.ts @@ -1,11 +1,16 @@ /* oxlint-disable @typescript-eslint/no-unsafe-call @typescript-eslint/no-unsafe-member-access */ import { createElement } from 'react'; import { act } from '@/test/renderer'; +import { 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'; +import type { QueryClient } from '@tanstack/react-query'; +// 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 +20,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 +68,7 @@ type OrganizationContextValue = { async function mountProvider(): Promise<{ getCtx: () => OrganizationContextValue; + queryClient: QueryClient; unmount: () => void; }> { let capturedCtx: OrganizationContextValue | undefined = undefined; @@ -88,6 +99,7 @@ async function mountProvider(): Promise<{ } return capturedCtx; }, + queryClient: ui.queryClient, unmount: ui.unmount, }; } @@ -179,3 +191,137 @@ describe('OrganizationProvider.setOrganizationId', () => { unmount(); }); }); + +describe('OrganizationProvider lost-membership recovery', () => { + 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. + hoisted.getItemAsync.mockImplementation(async key => + key === ORGANIZATION_STORAGE_KEY ? 'org-lost' : null + ); + 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 () => { + hoisted.getItemAsync.mockImplementation(async key => + key === ORGANIZATION_STORAGE_KEY ? 'org-lost' : null + ); + 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 () => { + hoisted.getItemAsync.mockImplementation(async key => + key === ORGANIZATION_STORAGE_KEY ? 'org-lost' : null + ); + 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')); + }); + 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('does not re-switch when the user picks another context during recovery', async () => { + hoisted.getItemAsync.mockImplementation(async key => + key === ORGANIZATION_STORAGE_KEY ? 'org-lost' : null + ); + 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..89a87e5b81e 100644 --- a/apps/mobile/src/lib/organization-context.tsx +++ b/apps/mobile/src/lib/organization-context.tsx @@ -16,6 +16,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 +79,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 +96,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 +222,49 @@ export function OrganizationProvider({ children }: { readonly children: ReactNod persist, ]); + // Detection: compare each settled, successful list snapshot against the one + // the current selection was published with. Only a NEW snapshot missing the + // stored organization means the membership vanished server-side; the + // selection's own snapshot is valid by construction. The recovery effect + // (declared below `setOrganizationId`, which it calls) owns the switch. + // Personal (null) needs no recovery, and the default resolution (above) + // already switched away from a list that emptied. + useEffect(() => { + if (!state.isLoaded || !organizationsFetched || organizationsError) { + selectionListSeen.current = undefined; + return; + } + if (organizationsFetching) { + return; + } + const snapshot = organizations ?? []; + if (selectionListSeen.current === undefined) { + // First settled snapshot for this selection: baseline, never stale. + selectionListSeen.current = snapshot; + return; + } + if (snapshot === selectionListSeen.current) { + return; + } + selectionListSeen.current = snapshot; + const selected = state.organizationId; + if (selected === null || state.error !== null) { + return; + } + if (snapshot.some(entry => entry.organizationId === selected)) { + 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 +276,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 +287,8 @@ export function OrganizationProvider({ children }: { readonly children: ReactNod const setOrganizationId = useCallback( (id: string | null) => { + // A manual selection disarms the recovery switch: the user moved first. + recoveringRef.current = undefined; // 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. @@ -268,6 +330,50 @@ export function OrganizationProvider({ children }: { readonly children: ReactNod [token, persist, state.organizationId] ); + // 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..f7b9a11317e 100644 --- a/apps/mobile/src/lib/query-client.test.ts +++ b/apps/mobile/src/lib/query-client.test.ts @@ -204,6 +204,77 @@ describe('permission-denied query removal', () => { }); }); +describe('organizations.list refresh on permission loss', () => { + 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 }, + }); + + 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 org-scoped FORBIDDEN lands while the list query has no observer: + // scheduling its refresh is still observable once one mounts, so the + // resolver gates the refetch this observer then triggers. + const pending = Promise.withResolvers<{ organizationId: string }[]>(); + listFn.mockImplementation(() => + listFn.mock.calls.length === 1 ? pending.promise : Promise.resolve([]) + ); + const listObserver = new QueryObserver(queryClient, { + queryKey: LIST_KEY, + queryFn: listFn, + retry: false, + staleTime: Infinity, + }); + const unsubList = listObserver.subscribe(() => undefined); + await flushQueryUpdates(); + // The triggered refetch arrived at the gated queryFn. + expect(listFn).toHaveBeenCalled(); + 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 and no refetch was scheduled: the + // cached payload is untouched and still fresh. + expect(queryClient.getQueryState(LIST_KEY)?.dataUpdatedAt).toBeGreaterThan(0); + }); +}); + 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..27e6bf12ed4 100644 --- a/apps/mobile/src/lib/query-client.ts +++ b/apps/mobile/src/lib/query-client.ts @@ -220,6 +220,34 @@ 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. Mark the shared list stale so it refetches; 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. + */ +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; + } + const listKey: QueryKey = [['organizations', 'list']]; + const list = queryClient.getQueryCache().find({ queryKey: listKey }); + if (!list || list.state.fetchStatus !== 'idle') { + return; + } + void queryClient.refetchQueries({ queryKey: listKey, stale: true }); +} + /** * 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 +288,7 @@ export function createKiloAppQueryClient(): QueryClient { if (!query.isActive()) { removePermissionDeniedQueries(queryClient, error, query); } + refreshOrganizationsListOnPermissionLoss(queryClient, error, query); }, }), mutationCache: new MutationCache({ From fd5e39cb320ae01f17aa1f15256a3e4458802304 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= <4062236+iscekic@users.noreply.github.com> Date: Mon, 19 Oct 2026 10:35:50 +0000 Subject: [PATCH 2/9] fix(mobile): refresh the organizations list through invalidation The permission-loss trigger refetched with a stale filter, which skipped a list inside its freshness window; invalidateQueries marks the payload stale and refetches active observers regardless of age. Also hoists the pure key/error helpers out of the describe block and satisfies the promise lint rules in the touched tests. --- .../src/lib/organization-context.test.ts | 32 +++++++------- apps/mobile/src/lib/organization-context.tsx | 6 ++- apps/mobile/src/lib/query-client.test.ts | 44 +++++++++++-------- apps/mobile/src/lib/query-client.ts | 14 +++--- 4 files changed, 53 insertions(+), 43 deletions(-) diff --git a/apps/mobile/src/lib/organization-context.test.ts b/apps/mobile/src/lib/organization-context.test.ts index cc7e0b7151f..cd41699970e 100644 --- a/apps/mobile/src/lib/organization-context.test.ts +++ b/apps/mobile/src/lib/organization-context.test.ts @@ -1,13 +1,12 @@ /* 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 { waitFor } from '@/test/render-with-providers'; +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'; -import type { QueryClient } from '@tanstack/react-query'; // 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'; @@ -193,6 +192,16 @@ describe('OrganizationProvider.setOrganizationId', () => { }); describe('OrganizationProvider lost-membership recovery', () => { + /** Stores the recovered fixture selection; every other key reads absent. */ + const storeSelection = (): void => { + hoisted.getItemAsync.mockImplementation( + async key => + await (key === ORGANIZATION_STORAGE_KEY + ? Promise.resolve('org-lost') + : Promise.resolve(null)) + ); + }; + beforeEach(() => { vi.clearAllMocks(); hoisted.useAuth.mockReturnValue({ token: 't' }); @@ -214,9 +223,7 @@ describe('OrganizationProvider lost-membership recovery', () => { 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. - hoisted.getItemAsync.mockImplementation(async key => - key === ORGANIZATION_STORAGE_KEY ? 'org-lost' : null - ); + storeSelection(); hoisted.list.mockResolvedValue([ { organizationId: 'org-lost', organizationName: 'Doomed org', role: 'member' }, { organizationId: 'org-kept', organizationName: 'Kept org', role: 'member' }, @@ -245,9 +252,7 @@ describe('OrganizationProvider lost-membership recovery', () => { }); it('falls back to Personal, writes the marker, and names Personal when no membership remains', async () => { - hoisted.getItemAsync.mockImplementation(async key => - key === ORGANIZATION_STORAGE_KEY ? 'org-lost' : null - ); + storeSelection(); hoisted.list.mockResolvedValue([ { organizationId: 'org-lost', organizationName: 'Doomed org', role: 'member' }, ]); @@ -270,9 +275,7 @@ describe('OrganizationProvider lost-membership recovery', () => { }); it('keeps the selection while the list is loading, failing, or still names the org', async () => { - hoisted.getItemAsync.mockImplementation(async key => - key === ORGANIZATION_STORAGE_KEY ? 'org-lost' : null - ); + storeSelection(); const pending = Promise.withResolvers<{ organizationId: string }[]>(); hoisted.list.mockReturnValue(pending.promise); const { getCtx, queryClient, unmount } = await mountProvider(); @@ -281,6 +284,7 @@ describe('OrganizationProvider lost-membership recovery', () => { // 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(); @@ -297,9 +301,7 @@ describe('OrganizationProvider lost-membership recovery', () => { }); it('does not re-switch when the user picks another context during recovery', async () => { - hoisted.getItemAsync.mockImplementation(async key => - key === ORGANIZATION_STORAGE_KEY ? 'org-lost' : null - ); + storeSelection(); hoisted.list.mockResolvedValue([ { organizationId: 'org-lost', organizationName: 'Doomed org', role: 'member' }, { organizationId: 'org-kept', organizationName: 'Kept org', role: 'member' }, diff --git a/apps/mobile/src/lib/organization-context.tsx b/apps/mobile/src/lib/organization-context.tsx index 89a87e5b81e..27d2b9f605e 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,7 +20,7 @@ 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 { 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'; diff --git a/apps/mobile/src/lib/query-client.test.ts b/apps/mobile/src/lib/query-client.test.ts index f7b9a11317e..e16f95c9550 100644 --- a/apps/mobile/src/lib/query-client.test.ts +++ b/apps/mobile/src/lib/query-client.test.ts @@ -204,17 +204,17 @@ describe('permission-denied query removal', () => { }); }); -describe('organizations.list refresh on permission loss', () => { - 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 }, - }); +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 }[]>>(); @@ -231,13 +231,21 @@ describe('organizations.list refresh on permission loss', () => { const unsubscribe = observer.subscribe(() => undefined); await observer.refetch().catch(() => undefined); unsubscribe(); - // The org-scoped FORBIDDEN lands while the list query has no observer: - // scheduling its refresh is still observable once one mounts, so the - // resolver gates the refetch this observer then triggers. + + // 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 }[]>(); - listFn.mockImplementation(() => - listFn.mock.calls.length === 1 ? pending.promise : Promise.resolve([]) - ); + 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 (listFn.mock.results.length > 1) { + return []; + } + return pending.promise; + }); const listObserver = new QueryObserver(queryClient, { queryKey: LIST_KEY, queryFn: listFn, @@ -246,8 +254,8 @@ describe('organizations.list refresh on permission loss', () => { }); const unsubList = listObserver.subscribe(() => undefined); await flushQueryUpdates(); - // The triggered refetch arrived at the gated queryFn. - expect(listFn).toHaveBeenCalled(); + // 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' }]); diff --git a/apps/mobile/src/lib/query-client.ts b/apps/mobile/src/lib/query-client.ts index 27e6bf12ed4..387940ebc08 100644 --- a/apps/mobile/src/lib/query-client.ts +++ b/apps/mobile/src/lib/query-client.ts @@ -224,9 +224,10 @@ function removePermissionDeniedQueries( * 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. Mark the shared list stale so it refetches; 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. + * 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, @@ -240,12 +241,7 @@ function refreshOrganizationsListOnPermissionLoss( if (organizationId === undefined || organizationId === null) { return; } - const listKey: QueryKey = [['organizations', 'list']]; - const list = queryClient.getQueryCache().find({ queryKey: listKey }); - if (!list || list.state.fetchStatus !== 'idle') { - return; - } - void queryClient.refetchQueries({ queryKey: listKey, stale: true }); + void queryClient.invalidateQueries({ queryKey: [['organizations', 'list']] }); } /** From 710bf491e25b8d9dacbe380d8edad322fb16bccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= <4062236+iscekic@users.noreply.github.com> Date: Mon, 19 Oct 2026 10:46:42 +0000 Subject: [PATCH 3/9] test(mobile): satisfy promise lint rules in touched suites --- apps/mobile/src/lib/organization-context.test.ts | 11 +++++------ apps/mobile/src/lib/query-client.test.ts | 8 ++++++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/apps/mobile/src/lib/organization-context.test.ts b/apps/mobile/src/lib/organization-context.test.ts index cd41699970e..67061649905 100644 --- a/apps/mobile/src/lib/organization-context.test.ts +++ b/apps/mobile/src/lib/organization-context.test.ts @@ -194,12 +194,11 @@ describe('OrganizationProvider.setOrganizationId', () => { describe('OrganizationProvider lost-membership recovery', () => { /** Stores the recovered fixture selection; every other key reads absent. */ const storeSelection = (): void => { - hoisted.getItemAsync.mockImplementation( - async key => - await (key === ORGANIZATION_STORAGE_KEY - ? Promise.resolve('org-lost') - : Promise.resolve(null)) - ); + hoisted.getItemAsync.mockImplementation(async key => { + const stored = key === ORGANIZATION_STORAGE_KEY ? 'org-lost' : null; + await Promise.resolve(); + return stored; + }); }; beforeEach(() => { diff --git a/apps/mobile/src/lib/query-client.test.ts b/apps/mobile/src/lib/query-client.test.ts index e16f95c9550..62aadb11f52 100644 --- a/apps/mobile/src/lib/query-client.test.ts +++ b/apps/mobile/src/lib/query-client.test.ts @@ -237,14 +237,18 @@ describe('organizations.list refresh on permission loss', () => { 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 (listFn.mock.results.length > 1) { + if (gated) { + await Promise.resolve(); return []; } - return pending.promise; + gated = true; + const settled = await pending.promise; + return settled; }); const listObserver = new QueryObserver(queryClient, { queryKey: LIST_KEY, From 2f369b3964ebe67ee182f42d88ff1cee1606f396 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= <4062236+iscekic@users.noreply.github.com> Date: Mon, 19 Oct 2026 13:07:34 +0000 Subject: [PATCH 4/9] fix(mobile): cover the cold-start recovery and review follow-ups A selection restored from storage had no list baseline, so on a cold start without a persisted read cache the first live snapshot already missing the organization became the baseline and recovery never armed. A restored selection now evaluates its very first snapshot; a manual pick records the snapshot it was made against as its baseline. The detection effect records the snapshot only after its guards pass, so a snapshot observed during a save or restore error is re-evaluated once the error clears. The list invalidation now passes cancelRefetch false so concurrent denials cannot cancel each other's shared fetch, and the non-org test asserts the invalidation flag instead of the fetch timestamp. --- .../src/lib/organization-context.test.ts | 19 ++++++++++ apps/mobile/src/lib/organization-context.tsx | 37 +++++++++---------- apps/mobile/src/lib/query-client.test.ts | 6 ++- apps/mobile/src/lib/query-client.ts | 8 +++- 4 files changed, 47 insertions(+), 23 deletions(-) diff --git a/apps/mobile/src/lib/organization-context.test.ts b/apps/mobile/src/lib/organization-context.test.ts index 67061649905..5c6e84b7899 100644 --- a/apps/mobile/src/lib/organization-context.test.ts +++ b/apps/mobile/src/lib/organization-context.test.ts @@ -299,6 +299,25 @@ describe('OrganizationProvider lost-membership recovery', () => { 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([ diff --git a/apps/mobile/src/lib/organization-context.tsx b/apps/mobile/src/lib/organization-context.tsx index 27d2b9f605e..345c25c441a 100644 --- a/apps/mobile/src/lib/organization-context.tsx +++ b/apps/mobile/src/lib/organization-context.tsx @@ -226,36 +226,29 @@ export function OrganizationProvider({ children }: { readonly children: ReactNod persist, ]); - // Detection: compare each settled, successful list snapshot against the one - // the current selection was published with. Only a NEW snapshot missing the - // stored organization means the membership vanished server-side; the - // selection's own snapshot is valid by construction. The recovery effect - // (declared below `setOrganizationId`, which it calls) owns the switch. - // Personal (null) needs no recovery, and the default resolution (above) - // already switched away from a list that emptied. + // Detection: a settled, successful snapshot that no longer names the stored + // organization arms the recovery effect (declared below `setOrganizationId`, + // which it calls). A pick records the snapshot it was made against (above); + // a restored selection has no baseline, so its very first snapshot is + // evaluated — a cold start whose list already misses the stored org recovers + // instead of trusting the dead selection. Personal (null) and the default + // resolution (above) need no detection. The baseline updates 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) { + if (organizationsFetching || state.error !== null || state.organizationId === null) { return; } const snapshot = organizations ?? []; - if (selectionListSeen.current === undefined) { - // First settled snapshot for this selection: baseline, never stale. - selectionListSeen.current = snapshot; - return; - } if (snapshot === selectionListSeen.current) { return; } - selectionListSeen.current = snapshot; - const selected = state.organizationId; - if (selected === null || state.error !== null) { - return; - } - if (snapshot.some(entry => entry.organizationId === selected)) { + if (snapshot.some(entry => entry.organizationId === state.organizationId)) { + selectionListSeen.current = snapshot; return; } recoveringRef.current = snapshot[0]?.organizationId ?? null; @@ -292,7 +285,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. + // The picker only offers orgs from the current snapshot, so that + // snapshot is the selection's baseline; the next snapshot missing the + // selection means the membership vanished after the pick. 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. @@ -331,7 +328,7 @@ 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 diff --git a/apps/mobile/src/lib/query-client.test.ts b/apps/mobile/src/lib/query-client.test.ts index 62aadb11f52..55805c115f7 100644 --- a/apps/mobile/src/lib/query-client.test.ts +++ b/apps/mobile/src/lib/query-client.test.ts @@ -281,9 +281,11 @@ describe('organizations.list refresh on permission loss', () => { }) ).rejects.toBe(error); - // No observer was attached to the list and no refetch was scheduled: the - // cached payload is untouched and still fresh. + // 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); }); }); diff --git a/apps/mobile/src/lib/query-client.ts b/apps/mobile/src/lib/query-client.ts index 387940ebc08..3ec26bf9911 100644 --- a/apps/mobile/src/lib/query-client.ts +++ b/apps/mobile/src/lib/query-client.ts @@ -241,7 +241,13 @@ function refreshOrganizationsListOnPermissionLoss( if (organizationId === undefined || organizationId === null) { return; } - void queryClient.invalidateQueries({ queryKey: [['organizations', 'list']] }); + // `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 } + ); } /** From 5124f72259e5548726f2cccde5dee97b234c3056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= <4062236+iscekic@users.noreply.github.com> Date: Mon, 19 Oct 2026 13:35:22 +0000 Subject: [PATCH 5/9] fix(mobile): translate the recovery notice into every catalog Adds organization.boundary.membershipLostNotice to all 86 non-English catalogs with the {{target}} placeholder kept exactly once per locale, matching the sibling boundary copy's tone and formality. --- apps/mobile/src/i18n/locales/af.json | 1 + apps/mobile/src/i18n/locales/am.json | 1 + apps/mobile/src/i18n/locales/ar.json | 1 + apps/mobile/src/i18n/locales/az.json | 1 + apps/mobile/src/i18n/locales/be.json | 1 + apps/mobile/src/i18n/locales/bg.json | 1 + apps/mobile/src/i18n/locales/bn.json | 1 + apps/mobile/src/i18n/locales/bs.json | 1 + apps/mobile/src/i18n/locales/ca.json | 1 + apps/mobile/src/i18n/locales/ckb.json | 1 + apps/mobile/src/i18n/locales/cs.json | 1 + apps/mobile/src/i18n/locales/cy.json | 1 + apps/mobile/src/i18n/locales/da.json | 1 + apps/mobile/src/i18n/locales/de.json | 1 + apps/mobile/src/i18n/locales/el.json | 1 + apps/mobile/src/i18n/locales/es.json | 1 + apps/mobile/src/i18n/locales/et.json | 1 + apps/mobile/src/i18n/locales/eu.json | 1 + apps/mobile/src/i18n/locales/fa.json | 1 + apps/mobile/src/i18n/locales/fi.json | 1 + apps/mobile/src/i18n/locales/fil.json | 1 + apps/mobile/src/i18n/locales/fr.json | 1 + apps/mobile/src/i18n/locales/ga.json | 1 + apps/mobile/src/i18n/locales/gl.json | 1 + apps/mobile/src/i18n/locales/gu.json | 1 + apps/mobile/src/i18n/locales/ha.json | 1 + apps/mobile/src/i18n/locales/he.json | 1 + apps/mobile/src/i18n/locales/hi.json | 1 + apps/mobile/src/i18n/locales/hr.json | 1 + apps/mobile/src/i18n/locales/ht.json | 1 + apps/mobile/src/i18n/locales/hu.json | 1 + apps/mobile/src/i18n/locales/hy.json | 1 + apps/mobile/src/i18n/locales/id.json | 1 + apps/mobile/src/i18n/locales/ig.json | 1 + apps/mobile/src/i18n/locales/is.json | 1 + apps/mobile/src/i18n/locales/it.json | 1 + apps/mobile/src/i18n/locales/ja.json | 1 + apps/mobile/src/i18n/locales/ka.json | 1 + apps/mobile/src/i18n/locales/kk.json | 1 + apps/mobile/src/i18n/locales/km.json | 1 + apps/mobile/src/i18n/locales/kn.json | 1 + apps/mobile/src/i18n/locales/ko.json | 1 + apps/mobile/src/i18n/locales/lo.json | 1 + apps/mobile/src/i18n/locales/lt.json | 1 + apps/mobile/src/i18n/locales/lv.json | 1 + apps/mobile/src/i18n/locales/mg.json | 1 + apps/mobile/src/i18n/locales/mi.json | 1 + apps/mobile/src/i18n/locales/mk.json | 1 + apps/mobile/src/i18n/locales/ml.json | 1 + apps/mobile/src/i18n/locales/mn.json | 1 + apps/mobile/src/i18n/locales/mr.json | 1 + apps/mobile/src/i18n/locales/ms.json | 1 + apps/mobile/src/i18n/locales/mt.json | 1 + apps/mobile/src/i18n/locales/my.json | 1 + apps/mobile/src/i18n/locales/nb.json | 1 + apps/mobile/src/i18n/locales/ne.json | 1 + apps/mobile/src/i18n/locales/nl.json | 1 + apps/mobile/src/i18n/locales/om.json | 1 + apps/mobile/src/i18n/locales/or.json | 1 + apps/mobile/src/i18n/locales/pa.json | 1 + apps/mobile/src/i18n/locales/pl.json | 1 + apps/mobile/src/i18n/locales/ps.json | 1 + apps/mobile/src/i18n/locales/pt-BR.json | 1 + apps/mobile/src/i18n/locales/pt.json | 1 + apps/mobile/src/i18n/locales/ro.json | 1 + apps/mobile/src/i18n/locales/ru.json | 1 + apps/mobile/src/i18n/locales/si.json | 1 + apps/mobile/src/i18n/locales/sk.json | 1 + apps/mobile/src/i18n/locales/sl.json | 1 + apps/mobile/src/i18n/locales/so.json | 1 + apps/mobile/src/i18n/locales/sq.json | 1 + apps/mobile/src/i18n/locales/sr.json | 1 + apps/mobile/src/i18n/locales/sv.json | 1 + apps/mobile/src/i18n/locales/sw.json | 1 + apps/mobile/src/i18n/locales/ta.json | 1 + apps/mobile/src/i18n/locales/te.json | 1 + apps/mobile/src/i18n/locales/th.json | 1 + apps/mobile/src/i18n/locales/tr.json | 1 + apps/mobile/src/i18n/locales/uk.json | 1 + apps/mobile/src/i18n/locales/ur.json | 1 + apps/mobile/src/i18n/locales/uz.json | 1 + apps/mobile/src/i18n/locales/vi.json | 1 + apps/mobile/src/i18n/locales/yo.json | 1 + apps/mobile/src/i18n/locales/zh-Hans.json | 1 + apps/mobile/src/i18n/locales/zh-Hant.json | 1 + apps/mobile/src/i18n/locales/zu.json | 1 + 86 files changed, 86 insertions(+) diff --git a/apps/mobile/src/i18n/locales/af.json b/apps/mobile/src/i18n/locales/af.json index e47684b6888..0f93293a55e 100644 --- a/apps/mobile/src/i18n/locales/af.json +++ b/apps/mobile/src/i18n/locales/af.json @@ -2975,6 +2975,7 @@ "backToProfile": "Terug na profiel", "loadErrorMessage": "Kontroleer jou verbinding en probeer weer.", "loadErrorTitle": "Jou organisasies kon nie gelaai word nie.", + "membershipLostNotice": "Jy het nie meer toegang tot hierdie organisasie nie. Oorgeskakel na {{target}}.", "organizationUnavailable": "Die organisasie is nie beskikbaar nie.", "selectDescription": "Kies 'n organisasie uit jou profiel om voort te gaan.", "selectOrganization": "Kies 'n organisasie", diff --git a/apps/mobile/src/i18n/locales/am.json b/apps/mobile/src/i18n/locales/am.json index 39d5e3d3df3..4b757191303 100644 --- a/apps/mobile/src/i18n/locales/am.json +++ b/apps/mobile/src/i18n/locales/am.json @@ -2975,6 +2975,7 @@ "backToProfile": "ወደ መገለጫ ይመለሱ", "loadErrorMessage": "ግንኙነትህን አረጋግጠህ እንደገና ሞክር።", "loadErrorTitle": "ድርጅቶችዎን መጫን አልተቻለም።", + "membershipLostNotice": "ይህን ድርጅት ከእንግዲህ የመድረስ ፈቃድ የለዎትም። ወደ {{target}} ተቀይሯል።", "organizationUnavailable": "ድርጅቱ አይገኝም።", "selectDescription": "ለመቀጠል ከመገለጫዎ ድርጅት ይምረጡ።", "selectOrganization": "ድርጅት ይምረጡ", diff --git a/apps/mobile/src/i18n/locales/ar.json b/apps/mobile/src/i18n/locales/ar.json index 462399bd21a..6cdd2029a2d 100644 --- a/apps/mobile/src/i18n/locales/ar.json +++ b/apps/mobile/src/i18n/locales/ar.json @@ -3127,6 +3127,7 @@ "backToProfile": "العودة إلى الملف الشخصي", "loadErrorMessage": "تحقق من اتصالك بالإنترنت وحاول مرة أخرى.", "loadErrorTitle": "تعذّر تحميل مؤسساتك.", + "membershipLostNotice": "لم يعد لديك حق الوصول إلى هذه المؤسسة. تم التبديل إلى {{target}}.", "organizationUnavailable": "المؤسسة غير متاحة.", "selectDescription": "اختر مؤسسة من ملفك الشخصي للمتابعة.", "selectOrganization": "اختيار مؤسسة", diff --git a/apps/mobile/src/i18n/locales/az.json b/apps/mobile/src/i18n/locales/az.json index 586be53b2a4..0d23859a180 100644 --- a/apps/mobile/src/i18n/locales/az.json +++ b/apps/mobile/src/i18n/locales/az.json @@ -2975,6 +2975,7 @@ "backToProfile": "Profilə qayıt", "loadErrorMessage": "Bağlantını yoxla və yenidən cəhd et.", "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}}.", "organizationUnavailable": "Təşkilat əlçatan deyil.", "selectDescription": "Davam etmək üçün profilindən təşkilat seç.", "selectOrganization": "Təşkilat seç", diff --git a/apps/mobile/src/i18n/locales/be.json b/apps/mobile/src/i18n/locales/be.json index 7615f1e4b0d..79afebd6c65 100644 --- a/apps/mobile/src/i18n/locales/be.json +++ b/apps/mobile/src/i18n/locales/be.json @@ -3055,6 +3055,7 @@ "backToProfile": "Назад да профілю", "loadErrorMessage": "Правер злучэнне і паспрабуй яшчэ раз.", "loadErrorTitle": "Не ўдалося загрузіць вашы арганізацыі.", + "membershipLostNotice": "У вас больш няма доступу да гэтай арганізацыі. Пераключана на {{target}}.", "organizationUnavailable": "Арганізацыя недаступная.", "selectDescription": "Каб працягнуць, выберыце арганізацыю ў сваім профілі.", "selectOrganization": "Выберыце арганізацыю", diff --git a/apps/mobile/src/i18n/locales/bg.json b/apps/mobile/src/i18n/locales/bg.json index a61ab1bc88d..60689cb792b 100644 --- a/apps/mobile/src/i18n/locales/bg.json +++ b/apps/mobile/src/i18n/locales/bg.json @@ -2975,6 +2975,7 @@ "backToProfile": "Обратно към профила", "loadErrorMessage": "Провери връзката си и опитай отново.", "loadErrorTitle": "Не успяхме да заредим организациите ти.", + "membershipLostNotice": "Вече нямаш достъп до тази организация. Превключено към {{target}}.", "organizationUnavailable": "Организацията е недостъпна.", "selectDescription": "Избери организация от профила си, за да продължиш.", "selectOrganization": "Избор на организация", diff --git a/apps/mobile/src/i18n/locales/bn.json b/apps/mobile/src/i18n/locales/bn.json index 08680a6f391..a74309b11e9 100644 --- a/apps/mobile/src/i18n/locales/bn.json +++ b/apps/mobile/src/i18n/locales/bn.json @@ -2975,6 +2975,7 @@ "backToProfile": "প্রোফাইলে ফিরে যান", "loadErrorMessage": "আপনার সংযোগ পরীক্ষা করে আবার চেষ্টা করুন।", "loadErrorTitle": "আপনার সংস্থাগুলো লোড করা যায়নি।", + "membershipLostNotice": "এই সংস্থায় আপনার আর অ্যাক্সেস নেই। স্যুইচ করা হয়েছে {{target}}-এ।", "organizationUnavailable": "সংস্থাটি পাওয়া যাচ্ছে না।", "selectDescription": "চালিয়ে যেতে আপনার প্রোফাইল থেকে একটি সংস্থা বেছে নিন।", "selectOrganization": "একটি সংস্থা নির্বাচন করুন", diff --git a/apps/mobile/src/i18n/locales/bs.json b/apps/mobile/src/i18n/locales/bs.json index e8c65c27283..6bee229c0e7 100644 --- a/apps/mobile/src/i18n/locales/bs.json +++ b/apps/mobile/src/i18n/locales/bs.json @@ -3015,6 +3015,7 @@ "backToProfile": "Nazad na profil", "loadErrorMessage": "Provjeri vezu i pokušaj ponovo.", "loadErrorTitle": "Nije moguće učitati tvoje organizacije.", + "membershipLostNotice": "Više nemaš pristup ovoj organizaciji. Prebačeno na {{target}}.", "organizationUnavailable": "Organizacija nije dostupna.", "selectDescription": "Odaberi organizaciju na svom profilu za nastavak.", "selectOrganization": "Odaberi organizaciju", diff --git a/apps/mobile/src/i18n/locales/ca.json b/apps/mobile/src/i18n/locales/ca.json index 4ea566c79e3..1a6a06b3ccc 100644 --- a/apps/mobile/src/i18n/locales/ca.json +++ b/apps/mobile/src/i18n/locales/ca.json @@ -3015,6 +3015,7 @@ "backToProfile": "Torna al perfil", "loadErrorMessage": "Comprova la connexió i torna-ho a provar.", "loadErrorTitle": "No s'han pogut carregar les teves organitzacions.", + "membershipLostNotice": "Ja no tens accés a aquesta organització. Canviat a {{target}}.", "organizationUnavailable": "L'organització no està disponible.", "selectDescription": "Tria una organització des del teu perfil per continuar.", "selectOrganization": "Selecciona una organització", diff --git a/apps/mobile/src/i18n/locales/ckb.json b/apps/mobile/src/i18n/locales/ckb.json index 8a5226e64fa..24315853854 100644 --- a/apps/mobile/src/i18n/locales/ckb.json +++ b/apps/mobile/src/i18n/locales/ckb.json @@ -2975,6 +2975,7 @@ "backToProfile": "گەڕانەوە بۆ پڕۆفایل", "loadErrorMessage": "پەیوەندییەکەت بپشکنە و دووبارە هەوڵ بدەوە.", "loadErrorTitle": "نەتوانرا ڕێکخراوەکانت بار بکرێن.", + "membershipLostNotice": "چیتر مۆڵەتی دەستگەیشتن بەم ڕێکخراوەت نییە. گۆڕدرا بۆ {{target}}.", "organizationUnavailable": "ڕێکخراوەکە بەردەست نییە.", "selectDescription": "لە پڕۆفایلەکەتەوە ڕێکخراوێک هەڵبژێرە بۆ بەردەوامبوون.", "selectOrganization": "هەڵبژاردنی ڕێکخراوێک", diff --git a/apps/mobile/src/i18n/locales/cs.json b/apps/mobile/src/i18n/locales/cs.json index 9d4f4b2c40c..5543dade50c 100644 --- a/apps/mobile/src/i18n/locales/cs.json +++ b/apps/mobile/src/i18n/locales/cs.json @@ -3055,6 +3055,7 @@ "backToProfile": "Zpět na profil", "loadErrorMessage": "Zkontroluj připojení a zkus to znovu.", "loadErrorTitle": "Vaše organizace se nepodařilo načíst.", + "membershipLostNotice": "Už nemáte přístup do této organizace. Přepnuto na {{target}}.", "organizationUnavailable": "Organizace není k dispozici.", "selectDescription": "Vyberte ve svém profilu organizaci a pokračujte.", "selectOrganization": "Vyberte organizaci", diff --git a/apps/mobile/src/i18n/locales/cy.json b/apps/mobile/src/i18n/locales/cy.json index ddef8355043..b3607403e53 100644 --- a/apps/mobile/src/i18n/locales/cy.json +++ b/apps/mobile/src/i18n/locales/cy.json @@ -3135,6 +3135,7 @@ "backToProfile": "Yn ôl i'r proffil", "loadErrorMessage": "Gwiria dy gysylltiad a rho gynnig arall arni.", "loadErrorTitle": "Methu llwytho'ch sefydliadau.", + "membershipLostNotice": "Nid oes gennych fynediad i'r sefydliad hwn mwyach. Wedi newid i {{target}}.", "organizationUnavailable": "Nid yw'r sefydliad ar gael.", "selectDescription": "Dewiswch sefydliad o'ch proffil i barhau.", "selectOrganization": "Dewis sefydliad", diff --git a/apps/mobile/src/i18n/locales/da.json b/apps/mobile/src/i18n/locales/da.json index 6f722a602b8..86a368952f2 100644 --- a/apps/mobile/src/i18n/locales/da.json +++ b/apps/mobile/src/i18n/locales/da.json @@ -2975,6 +2975,7 @@ "backToProfile": "Tilbage til profil", "loadErrorMessage": "Tjek din forbindelse og prøv igen.", "loadErrorTitle": "Kunne ikke indlæse dine organisationer.", + "membershipLostNotice": "Du har ikke længere adgang til denne organisation. Skiftet til {{target}}.", "organizationUnavailable": "Organisationen er ikke tilgængelig.", "selectDescription": "Vælg en organisation på din profil for at fortsætte.", "selectOrganization": "Vælg en organisation", diff --git a/apps/mobile/src/i18n/locales/de.json b/apps/mobile/src/i18n/locales/de.json index 64239c457a5..5f342977e16 100644 --- a/apps/mobile/src/i18n/locales/de.json +++ b/apps/mobile/src/i18n/locales/de.json @@ -2967,6 +2967,7 @@ "backToProfile": "Zurück zum Profil", "loadErrorMessage": "Prüfe deine Verbindung und versuche es erneut.", "loadErrorTitle": "Deine Organisationen konnten nicht geladen werden.", + "membershipLostNotice": "Du hast keinen Zugriff mehr auf diese Organisation. Gewechselt zu {{target}}.", "organizationUnavailable": "Die Organisation ist nicht verfügbar.", "selectDescription": "Wähle in deinem Profil eine Organisation aus, um fortzufahren.", "selectOrganization": "Organisation auswählen", diff --git a/apps/mobile/src/i18n/locales/el.json b/apps/mobile/src/i18n/locales/el.json index 6f703da0736..9fffbda793a 100644 --- a/apps/mobile/src/i18n/locales/el.json +++ b/apps/mobile/src/i18n/locales/el.json @@ -2975,6 +2975,7 @@ "backToProfile": "Επιστροφή στο προφίλ", "loadErrorMessage": "Έλεγξε τη σύνδεσή σου και δοκίμασε ξανά.", "loadErrorTitle": "Δεν ήταν δυνατή η φόρτωση των οργανισμών σου.", + "membershipLostNotice": "Δεν έχεις πλέον πρόσβαση σε αυτόν τον οργανισμό. Έγινε αλλαγή σε {{target}}.", "organizationUnavailable": "Ο οργανισμός δεν είναι διαθέσιμος.", "selectDescription": "Επίλεξε έναν οργανισμό από το προφίλ σου για να συνεχίσεις.", "selectOrganization": "Επιλογή οργανισμού", diff --git a/apps/mobile/src/i18n/locales/es.json b/apps/mobile/src/i18n/locales/es.json index ec534452d62..604265e1966 100644 --- a/apps/mobile/src/i18n/locales/es.json +++ b/apps/mobile/src/i18n/locales/es.json @@ -3007,6 +3007,7 @@ "backToProfile": "Volver al perfil", "loadErrorMessage": "Comprueba tu conexión e inténtalo de nuevo.", "loadErrorTitle": "No se pudieron cargar tus organizaciones.", + "membershipLostNotice": "Ya no tienes acceso a esta organización. Cambiado a {{target}}.", "organizationUnavailable": "La organización no está disponible.", "selectDescription": "Elige una organización de tu perfil para continuar.", "selectOrganization": "Seleccionar una organización", diff --git a/apps/mobile/src/i18n/locales/et.json b/apps/mobile/src/i18n/locales/et.json index e09466a79fd..a7498856211 100644 --- a/apps/mobile/src/i18n/locales/et.json +++ b/apps/mobile/src/i18n/locales/et.json @@ -2975,6 +2975,7 @@ "backToProfile": "Tagasi profiilile", "loadErrorMessage": "Kontrolli ühendust ja proovi uuesti.", "loadErrorTitle": "Sinu organisatsioone ei õnnestunud laadida.", + "membershipLostNotice": "Sul puudub enam juurdepääs sellele organisatsioonile. Vahetatud {{target}}-le.", "organizationUnavailable": "Organisatsioon pole saadaval.", "selectDescription": "Jätkamiseks vali oma profiililt organisatsioon.", "selectOrganization": "Vali organisatsioon", diff --git a/apps/mobile/src/i18n/locales/eu.json b/apps/mobile/src/i18n/locales/eu.json index d652b846c75..5a16ce6d6a6 100644 --- a/apps/mobile/src/i18n/locales/eu.json +++ b/apps/mobile/src/i18n/locales/eu.json @@ -2975,6 +2975,7 @@ "backToProfile": "Itzuli profilera", "loadErrorMessage": "Egiaztatu konexioa eta saiatu berriro.", "loadErrorTitle": "Ezin izan dira zure erakundeak kargatu.", + "membershipLostNotice": "Ez duzu jada erakunde honetarako sarbiderik. Aldatu da {{target}}-ra.", "organizationUnavailable": "Erakundea ez dago erabilgarri.", "selectDescription": "Aukeratu erakunde bat zure profiletik jarraitzeko.", "selectOrganization": "Hautatu erakunde bat", diff --git a/apps/mobile/src/i18n/locales/fa.json b/apps/mobile/src/i18n/locales/fa.json index f710b28ea4d..d8749f3bc8a 100644 --- a/apps/mobile/src/i18n/locales/fa.json +++ b/apps/mobile/src/i18n/locales/fa.json @@ -2975,6 +2975,7 @@ "backToProfile": "بازگشت به پروفایل", "loadErrorMessage": "اتصال اینترنت خود را بررسی کنید و دوباره تلاش کنید.", "loadErrorTitle": "بارگذاری سازمان‌های شما ممکن نشد.", + "membershipLostNotice": "شما دیگر به این سازمان دسترسی ندارید. به {{target}} تغییر یافت.", "organizationUnavailable": "سازمان در دسترس نیست.", "selectDescription": "برای ادامه، در پروفایل خود یک سازمان انتخاب کنید.", "selectOrganization": "انتخاب سازمان", diff --git a/apps/mobile/src/i18n/locales/fi.json b/apps/mobile/src/i18n/locales/fi.json index 661b377748d..8ed60774fb2 100644 --- a/apps/mobile/src/i18n/locales/fi.json +++ b/apps/mobile/src/i18n/locales/fi.json @@ -2975,6 +2975,7 @@ "backToProfile": "Takaisin profiiliin", "loadErrorMessage": "Tarkista yhteys ja yritä uudelleen.", "loadErrorTitle": "Organisaatioitasi ei voitu ladata.", + "membershipLostNotice": "Sinulla ei ole enää pääsyä tähän organisaatioon. Vaihdettu {{target}}.", "organizationUnavailable": "Organisaatio ei ole käytettävissä.", "selectDescription": "Jatka valitsemalla organisaatio profiilistasi.", "selectOrganization": "Valitse organisaatio", diff --git a/apps/mobile/src/i18n/locales/fil.json b/apps/mobile/src/i18n/locales/fil.json index e9b55889e39..75df2804957 100644 --- a/apps/mobile/src/i18n/locales/fil.json +++ b/apps/mobile/src/i18n/locales/fil.json @@ -2975,6 +2975,7 @@ "backToProfile": "Bumalik sa profile", "loadErrorMessage": "Suriin ang koneksyon mo at subukan muli.", "loadErrorTitle": "Hindi ma-load ang iyong mga organisasyon.", + "membershipLostNotice": "Wala ka nang access sa organisasyong ito. Lumipat sa {{target}}.", "organizationUnavailable": "Hindi ma-access ang organisasyon.", "selectDescription": "Pumili ng organisasyon mula sa iyong profile para magpatuloy.", "selectOrganization": "Pumili ng organisasyon", diff --git a/apps/mobile/src/i18n/locales/fr.json b/apps/mobile/src/i18n/locales/fr.json index 5062352a40e..86e04d915a4 100644 --- a/apps/mobile/src/i18n/locales/fr.json +++ b/apps/mobile/src/i18n/locales/fr.json @@ -3007,6 +3007,7 @@ "backToProfile": "Retour au profil", "loadErrorMessage": "Vérifiez votre connexion et réessayez.", "loadErrorTitle": "Impossible de charger vos organisations.", + "membershipLostNotice": "Vous n'avez plus accès à cette organisation. Basculé vers {{target}}.", "organizationUnavailable": "L'organisation n'est pas disponible.", "selectDescription": "Choisissez une organisation dans votre profil pour continuer.", "selectOrganization": "Choix d'une organisation", diff --git a/apps/mobile/src/i18n/locales/ga.json b/apps/mobile/src/i18n/locales/ga.json index 4fbe765f79c..a60ac12246d 100644 --- a/apps/mobile/src/i18n/locales/ga.json +++ b/apps/mobile/src/i18n/locales/ga.json @@ -3095,6 +3095,7 @@ "backToProfile": "Ar ais go dtí an phróifíl", "loadErrorMessage": "Seiceáil do nasc agus bain triail eile as.", "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}}.", "organizationUnavailable": "Níl an eagraíocht ar fáil.", "selectDescription": "Roghnaigh eagraíocht ó do phróifíl le leanúint ar aghaidh.", "selectOrganization": "Roghnaigh eagraíocht", diff --git a/apps/mobile/src/i18n/locales/gl.json b/apps/mobile/src/i18n/locales/gl.json index 48c9d684d52..4bfa5910ed6 100644 --- a/apps/mobile/src/i18n/locales/gl.json +++ b/apps/mobile/src/i18n/locales/gl.json @@ -2975,6 +2975,7 @@ "backToProfile": "Volver ao perfil", "loadErrorMessage": "Comproba a túa conexión e téntao de novo.", "loadErrorTitle": "Non se puideron cargar as túas organizacións.", + "membershipLostNotice": "Xa non tes acceso a esta organización. Cambiado a {{target}}.", "organizationUnavailable": "A organización non está dispoñible.", "selectDescription": "Escolle unha organización desde o teu perfil para continuar.", "selectOrganization": "Seleccionar unha organización", diff --git a/apps/mobile/src/i18n/locales/gu.json b/apps/mobile/src/i18n/locales/gu.json index 501c3aa89ca..1d5f393cc36 100644 --- a/apps/mobile/src/i18n/locales/gu.json +++ b/apps/mobile/src/i18n/locales/gu.json @@ -2975,6 +2975,7 @@ "backToProfile": "પ્રોફાઇલ પર પાછા જાઓ", "loadErrorMessage": "તમારું ઇન્ટરનેટ જોડાણ તપાસીને ફરી પ્રયાસ કરો.", "loadErrorTitle": "તમારી સંસ્થાઓ લોડ કરી શકાઈ નહીં.", + "membershipLostNotice": "તમારી પાસે આ સંસ્થાનો ઍક્સેસ હવે નથી. {{target}} પર સ્વિચ કર્યું.", "organizationUnavailable": "સંસ્થા ઉપલબ્ધ નથી.", "selectDescription": "આગળ વધવા માટે તમારી પ્રોફાઇલમાંથી સંસ્થા પસંદ કરો.", "selectOrganization": "સંસ્થા પસંદ કરો", diff --git a/apps/mobile/src/i18n/locales/ha.json b/apps/mobile/src/i18n/locales/ha.json index 19a958fb5bd..7001ece66d3 100644 --- a/apps/mobile/src/i18n/locales/ha.json +++ b/apps/mobile/src/i18n/locales/ha.json @@ -2975,6 +2975,7 @@ "backToProfile": "Koma zuwa bayanan martaba", "loadErrorMessage": "Duba haɗin yanar gizonka, sannan ka sake gwadawa.", "loadErrorTitle": "An kasa loda ƙungiyoyinka.", + "membershipLostNotice": "Ba ka da izinin shiga wannan ƙungiyar a yanzu. An sauya zuwa {{target}}.", "organizationUnavailable": "Ba a samun ƙungiyar.", "selectDescription": "Zaɓi ƙungiya daga bayanan martabarka don ci gaba.", "selectOrganization": "Zaɓi ƙungiya", diff --git a/apps/mobile/src/i18n/locales/he.json b/apps/mobile/src/i18n/locales/he.json index 9570ab7598c..936191f800b 100644 --- a/apps/mobile/src/i18n/locales/he.json +++ b/apps/mobile/src/i18n/locales/he.json @@ -3007,6 +3007,7 @@ "backToProfile": "חזרה לפרופיל", "loadErrorMessage": "כדאי לבדוק את החיבור לאינטרנט ולנסות שוב.", "loadErrorTitle": "לא ניתן היה לטעון את הארגונים שלך.", + "membershipLostNotice": "אין לך גישה לארגון זה עוד. הוחלף ל-{{target}}.", "organizationUnavailable": "הארגון אינו זמין.", "selectDescription": "בחר ארגון מהפרופיל שלך כדי להמשיך.", "selectOrganization": "בחירת ארגון", diff --git a/apps/mobile/src/i18n/locales/hi.json b/apps/mobile/src/i18n/locales/hi.json index c773d33636b..f27844d000d 100644 --- a/apps/mobile/src/i18n/locales/hi.json +++ b/apps/mobile/src/i18n/locales/hi.json @@ -2967,6 +2967,7 @@ "backToProfile": "प्रोफ़ाइल पर वापस जाएँ", "loadErrorMessage": "अपना कनेक्शन जाँचें और फिर से कोशिश करें।", "loadErrorTitle": "आपके संगठन लोड नहीं हो सके।", + "membershipLostNotice": "आपको इस संगठन का ऐक्सेस अब नहीं है। {{target}} पर स्विच किया गया।", "organizationUnavailable": "संगठन उपलब्ध नहीं है।", "selectDescription": "जारी रखने के लिए अपनी प्रोफ़ाइल से एक संगठन चुनें।", "selectOrganization": "एक संगठन चुनें", diff --git a/apps/mobile/src/i18n/locales/hr.json b/apps/mobile/src/i18n/locales/hr.json index 1c819c45ea7..7556a1006bb 100644 --- a/apps/mobile/src/i18n/locales/hr.json +++ b/apps/mobile/src/i18n/locales/hr.json @@ -3015,6 +3015,7 @@ "backToProfile": "Natrag na profil", "loadErrorMessage": "Provjeri vezu i pokušaj ponovno.", "loadErrorTitle": "Nije moguće učitati tvoje organizacije.", + "membershipLostNotice": "Više nemaš pristup ovoj organizaciji. Prebačeno na {{target}}.", "organizationUnavailable": "Organizacija nije dostupna.", "selectDescription": "Za nastavak odaberi organizaciju na svom profilu.", "selectOrganization": "Odaberi organizaciju", diff --git a/apps/mobile/src/i18n/locales/ht.json b/apps/mobile/src/i18n/locales/ht.json index 9f76adf6e03..6c6e92c91c6 100644 --- a/apps/mobile/src/i18n/locales/ht.json +++ b/apps/mobile/src/i18n/locales/ht.json @@ -2975,6 +2975,7 @@ "backToProfile": "Tounen nan pwofil la", "loadErrorMessage": "Tcheke koneksyon ou epi eseye ankò.", "loadErrorTitle": "Nou pa t ka chaje òganizasyon ou yo.", + "membershipLostNotice": "Ou pa gen aksè nan òganizasyon sa a ankò. Chanje pou {{target}}.", "organizationUnavailable": "Òganizasyon an pa disponib.", "selectDescription": "Chwazi yon òganizasyon nan pwofil ou pou kontinye.", "selectOrganization": "Chwazi yon òganizasyon", diff --git a/apps/mobile/src/i18n/locales/hu.json b/apps/mobile/src/i18n/locales/hu.json index 0532c58fba5..b86d822e69e 100644 --- a/apps/mobile/src/i18n/locales/hu.json +++ b/apps/mobile/src/i18n/locales/hu.json @@ -2975,6 +2975,7 @@ "backToProfile": "Vissza a profilhoz", "loadErrorMessage": "Ellenőrizd az internetkapcsolatot, és próbáld újra.", "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}}.", "organizationUnavailable": "A szervezet nem érhető el.", "selectDescription": "A folytatáshoz válassz szervezetet a profilodból.", "selectOrganization": "Szervezet kiválasztása", diff --git a/apps/mobile/src/i18n/locales/hy.json b/apps/mobile/src/i18n/locales/hy.json index b42c8c9458e..53dfa1e5c91 100644 --- a/apps/mobile/src/i18n/locales/hy.json +++ b/apps/mobile/src/i18n/locales/hy.json @@ -2975,6 +2975,7 @@ "backToProfile": "Վերադառնալ պրոֆիլին", "loadErrorMessage": "Ստուգիր ինտերնետ կապը և կրկին փորձիր։", "loadErrorTitle": "Չհաջողվեց բեռնել ձեր կազմակերպությունները։", + "membershipLostNotice": "Դուք այլևս այս կազմակերպություն մուտք գործելու իրավունք չունեք։ Փոխարկվել է {{target}}։", "organizationUnavailable": "Կազմակերպությունը հասանելի չէ։", "selectDescription": "Շարունակելու համար ձեր պրոֆիլում ընտրեք կազմակերպություն։", "selectOrganization": "Ընտրել կազմակերպություն", diff --git a/apps/mobile/src/i18n/locales/id.json b/apps/mobile/src/i18n/locales/id.json index 7318a76b5dd..bd1b770fb70 100644 --- a/apps/mobile/src/i18n/locales/id.json +++ b/apps/mobile/src/i18n/locales/id.json @@ -2967,6 +2967,7 @@ "backToProfile": "Kembali ke profil", "loadErrorMessage": "Periksa koneksimu dan coba lagi.", "loadErrorTitle": "Tidak dapat memuat daftar organisasimu.", + "membershipLostNotice": "Kamu tidak lagi memiliki akses ke organisasi ini. Beralih ke {{target}}.", "organizationUnavailable": "Organisasi tidak tersedia.", "selectDescription": "Pilih organisasi dari profilmu untuk melanjutkan.", "selectOrganization": "Pilih organisasi", diff --git a/apps/mobile/src/i18n/locales/ig.json b/apps/mobile/src/i18n/locales/ig.json index 5d3c37a14f4..6ba8a41827c 100644 --- a/apps/mobile/src/i18n/locales/ig.json +++ b/apps/mobile/src/i18n/locales/ig.json @@ -2975,6 +2975,7 @@ "backToProfile": "Laghachi na profaịlụ", "loadErrorMessage": "Lelee njikọ gị ma nwaa ọzọ.", "loadErrorTitle": "Enweghị ike ibudata ụlọ ọrụ gị.", + "membershipLostNotice": "Ị nweghịzi ikike ịbanye n'ụlọ ọrụ a. A gbanwere gaa na {{target}}.", "organizationUnavailable": "Ụlọ ọrụ adịghị.", "selectDescription": "Họrọ ụlọ ọrụ site na profaịlụ gị ka ị gaa n'ihu.", "selectOrganization": "Họrọ ụlọ ọrụ", diff --git a/apps/mobile/src/i18n/locales/is.json b/apps/mobile/src/i18n/locales/is.json index 7542b7cb67f..c9d4c1f9ed7 100644 --- a/apps/mobile/src/i18n/locales/is.json +++ b/apps/mobile/src/i18n/locales/is.json @@ -2975,6 +2975,7 @@ "backToProfile": "Til baka í prófíl", "loadErrorMessage": "Athugaðu tenginguna og reyndu aftur.", "loadErrorTitle": "Ekki tókst að hlaða félögunum þínum.", + "membershipLostNotice": "Þú hefur ekki lengur aðgang að þessu félagi. Skipt yfir í {{target}}.", "organizationUnavailable": "Félagið er ekki tiltækt.", "selectDescription": "Veldu félag úr prófílnum þínum til að halda áfram.", "selectOrganization": "Val á félagi", diff --git a/apps/mobile/src/i18n/locales/it.json b/apps/mobile/src/i18n/locales/it.json index 59154658f25..7386c79d66f 100644 --- a/apps/mobile/src/i18n/locales/it.json +++ b/apps/mobile/src/i18n/locales/it.json @@ -3007,6 +3007,7 @@ "backToProfile": "Torna al profilo", "loadErrorMessage": "Controlla la connessione e riprova.", "loadErrorTitle": "Impossibile caricare le tue organizzazioni.", + "membershipLostNotice": "Non hai più accesso a questa organizzazione. Passato a {{target}}.", "organizationUnavailable": "L'organizzazione non è disponibile.", "selectDescription": "Scegli un'organizzazione dal tuo profilo per continuare.", "selectOrganization": "Seleziona un'organizzazione", diff --git a/apps/mobile/src/i18n/locales/ja.json b/apps/mobile/src/i18n/locales/ja.json index 98bb71b5b9f..8e8af967782 100644 --- a/apps/mobile/src/i18n/locales/ja.json +++ b/apps/mobile/src/i18n/locales/ja.json @@ -2967,6 +2967,7 @@ "backToProfile": "プロフィールに戻る", "loadErrorMessage": "接続を確認して、もう一度お試しください。", "loadErrorTitle": "組織を読み込めませんでした。", + "membershipLostNotice": "この組織にアクセスできなくなりました。{{target}}に切り替えました。", "organizationUnavailable": "組織を利用できません。", "selectDescription": "続行するには、プロフィールから組織を選択してください。", "selectOrganization": "組織を選択", diff --git a/apps/mobile/src/i18n/locales/ka.json b/apps/mobile/src/i18n/locales/ka.json index c92dc780d18..10ae1b5c6b2 100644 --- a/apps/mobile/src/i18n/locales/ka.json +++ b/apps/mobile/src/i18n/locales/ka.json @@ -2975,6 +2975,7 @@ "backToProfile": "პროფილზე დაბრუნება", "loadErrorMessage": "შეამოწმე კავშირი და ხელახლა სცადე.", "loadErrorTitle": "თქვენი ორგანიზაციების ჩატვირთვა ვერ მოხერხდა.", + "membershipLostNotice": "თქვენ აღარ გაქვთ წვდომა ამ ორგანიზაციაზე. გადავერთეთ {{target}}-ზე.", "organizationUnavailable": "ორგანიზაცია მიუწვდომელია.", "selectDescription": "გასაგრძელებლად აირჩიეთ ორგანიზაცია თქვენი პროფილიდან.", "selectOrganization": "აირჩიეთ ორგანიზაცია", diff --git a/apps/mobile/src/i18n/locales/kk.json b/apps/mobile/src/i18n/locales/kk.json index 621f8b8995a..a1ec035ab12 100644 --- a/apps/mobile/src/i18n/locales/kk.json +++ b/apps/mobile/src/i18n/locales/kk.json @@ -2975,6 +2975,7 @@ "backToProfile": "Профильге оралу", "loadErrorMessage": "Интернетке қосылғаныңызды тексеріп, қайталап көріңіз.", "loadErrorTitle": "Ұйымдарыңызды жүктеу мүмкін болмады.", + "membershipLostNotice": "Бұл ұйымға енді кіру рұқсатыңыз жоқ. {{target}}-ға ауыстырылды.", "organizationUnavailable": "Ұйым қолжетімсіз.", "selectDescription": "Жалғастыру үшін профильден ұйымды таңдаңыз.", "selectOrganization": "Ұйымды таңдау", diff --git a/apps/mobile/src/i18n/locales/km.json b/apps/mobile/src/i18n/locales/km.json index 3845f66b5da..b836b426c2f 100644 --- a/apps/mobile/src/i18n/locales/km.json +++ b/apps/mobile/src/i18n/locales/km.json @@ -2975,6 +2975,7 @@ "backToProfile": "ត្រឡប់ទៅប្រវត្តិរូប", "loadErrorMessage": "សូមពិនិត្យការតភ្ជាប់របស់អ្នក ហើយព្យាយាមម្តងទៀត។", "loadErrorTitle": "មិនអាចផ្ទុកអង្គការរបស់អ្នកបានទេ។", + "membershipLostNotice": "អ្នកលែងមានសិទ្ធិចូលប្រើអង្គការនេះទៀតហើយ។ បានប្ដូរទៅ {{target}}។", "organizationUnavailable": "មិនអាចចូលប្រើអង្គការបានទេ។", "selectDescription": "ជ្រើសរើសអង្គការពីប្រវត្តិរូបរបស់អ្នកដើម្បីបន្ត។", "selectOrganization": "ជ្រើសរើសអង្គការ", diff --git a/apps/mobile/src/i18n/locales/kn.json b/apps/mobile/src/i18n/locales/kn.json index 52c467f95c4..065b1ca0f18 100644 --- a/apps/mobile/src/i18n/locales/kn.json +++ b/apps/mobile/src/i18n/locales/kn.json @@ -2975,6 +2975,7 @@ "backToProfile": "ಪ್ರೊಫೈಲ್‌ಗೆ ಹಿಂತಿರುಗಿ", "loadErrorMessage": "ನಿಮ್ಮ ಸಂಪರ್ಕವನ್ನು ಪರಿಶೀಲಿಸಿ ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ.", "loadErrorTitle": "ನಿಮ್ಮ ಸಂಸ್ಥೆಗಳನ್ನು ಲೋಡ್ ಮಾಡಲು ಸಾಧ್ಯವಾಗಲಿಲ್ಲ.", + "membershipLostNotice": "ಈ ಸಂಸ್ಥೆಗೆ ನಿಮಗೆ ಇನ್ನು ಪ್ರವೇಶವಿಲ್ಲ. {{target}}ಗೆ ಬದಲಾಯಿಸಲಾಗಿದೆ.", "organizationUnavailable": "ಸಂಸ್ಥೆ ಲಭ್ಯವಿಲ್ಲ.", "selectDescription": "ಮುಂದುವರಿಯಲು ನಿಮ್ಮ ಪ್ರೊಫೈಲ್‌ನಿಂದ ಸಂಸ್ಥೆಯನ್ನು ಆಯ್ಕೆಮಾಡಿ.", "selectOrganization": "ಸಂಸ್ಥೆಯನ್ನು ಆಯ್ಕೆಮಾಡಿ", diff --git a/apps/mobile/src/i18n/locales/ko.json b/apps/mobile/src/i18n/locales/ko.json index 63509ba0bec..27ac93c61c2 100644 --- a/apps/mobile/src/i18n/locales/ko.json +++ b/apps/mobile/src/i18n/locales/ko.json @@ -2967,6 +2967,7 @@ "backToProfile": "프로필로 돌아가기", "loadErrorMessage": "인터넷 연결을 확인하고 다시 시도하세요.", "loadErrorTitle": "조직 목록을 불러올 수 없습니다.", + "membershipLostNotice": "이 조직에 더 이상 접근할 수 없습니다. {{target}}(으)로 전환되었습니다.", "organizationUnavailable": "조직을 사용할 수 없습니다.", "selectDescription": "계속하려면 프로필에서 조직을 선택하세요.", "selectOrganization": "조직 선택", diff --git a/apps/mobile/src/i18n/locales/lo.json b/apps/mobile/src/i18n/locales/lo.json index a7c92be80f0..da15e62c383 100644 --- a/apps/mobile/src/i18n/locales/lo.json +++ b/apps/mobile/src/i18n/locales/lo.json @@ -2975,6 +2975,7 @@ "backToProfile": "ກັບໄປທີ່ໂປຣໄຟລ໌", "loadErrorMessage": "ກວດສອບການເຊື່ອມຕໍ່ ແລ້ວລອງໃໝ່.", "loadErrorTitle": "ບໍ່ສາມາດໂຫຼດລາຍຊື່ອົງກອນຂອງທ່ານໄດ້.", + "membershipLostNotice": "ທ່ານບໍ່ມີສິດເຂົ້າເຖິງອົງກອນນີ້ອີກແລ້ວ. ປ່ຽນໄປ {{target}} ແລ້ວ.", "organizationUnavailable": "ບໍ່ສາມາດໃຊ້ງານອົງກອນນີ້ໄດ້.", "selectDescription": "ເລືອກອົງກອນຈາກໂປຣໄຟລ໌ຂອງທ່ານເພື່ອສືບຕໍ່.", "selectOrganization": "ເລືອກອົງກອນ", diff --git a/apps/mobile/src/i18n/locales/lt.json b/apps/mobile/src/i18n/locales/lt.json index cb8912c8c3c..e9daa8128cd 100644 --- a/apps/mobile/src/i18n/locales/lt.json +++ b/apps/mobile/src/i18n/locales/lt.json @@ -3055,6 +3055,7 @@ "backToProfile": "Grįžti į profilį", "loadErrorMessage": "Patikrink ryšį ir bandyk dar kartą.", "loadErrorTitle": "Nepavyko įkelti jūsų organizacijų.", + "membershipLostNotice": "Jūs nebeturite prieigos prie šios organizacijos. Perjungta į {{target}}.", "organizationUnavailable": "Organizacija nepasiekiama.", "selectDescription": "Norėdami tęsti, savo profilyje pasirinkite organizaciją.", "selectOrganization": "Pasirinkite organizaciją", diff --git a/apps/mobile/src/i18n/locales/lv.json b/apps/mobile/src/i18n/locales/lv.json index ed20894a025..fa2d8416c47 100644 --- a/apps/mobile/src/i18n/locales/lv.json +++ b/apps/mobile/src/i18n/locales/lv.json @@ -3015,6 +3015,7 @@ "backToProfile": "Atpakaļ uz profilu", "loadErrorMessage": "Pārbaudi savienojumu un mēģini vēlreiz.", "loadErrorTitle": "Neizdevās ielādēt tavas organizācijas.", + "membershipLostNotice": "Tev vairs nav piekļuves šai organizācijai. Pārslēgts uz {{target}}.", "organizationUnavailable": "Organizācija nav pieejama.", "selectDescription": "Lai turpinātu, izvēlies organizāciju savā profilā.", "selectOrganization": "Organizācijas izvēle", diff --git a/apps/mobile/src/i18n/locales/mg.json b/apps/mobile/src/i18n/locales/mg.json index 9e29ac0a424..11138cac5b2 100644 --- a/apps/mobile/src/i18n/locales/mg.json +++ b/apps/mobile/src/i18n/locales/mg.json @@ -2975,6 +2975,7 @@ "backToProfile": "Miverena amin'ny mombamomba anao", "loadErrorMessage": "Jereo ny fifandraisanao ary andramo indray.", "loadErrorTitle": "Tsy voaray ny fikambananao.", + "membershipLostNotice": "Tsy manana alalana hiditra amin'ity fikambanana ity intsony ianao. Nafindra tany amin'ny {{target}}.", "organizationUnavailable": "Tsy azo idirana ny fikambanana.", "selectDescription": "Misafidiana fikambanana ao amin'ny mombamomba anao mba hanohizana.", "selectOrganization": "Misafidiana fikambanana", diff --git a/apps/mobile/src/i18n/locales/mi.json b/apps/mobile/src/i18n/locales/mi.json index 715902e71ab..490d11b3c44 100644 --- a/apps/mobile/src/i18n/locales/mi.json +++ b/apps/mobile/src/i18n/locales/mi.json @@ -2975,6 +2975,7 @@ "backToProfile": "Hoki ki te kōtaha", "loadErrorMessage": "Tirohia tō hononga, ā, whakamātau anō.", "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}}.", "organizationUnavailable": "Kāore e wātea ana te rōpū.", "selectDescription": "Kōwhiria tētahi rōpū mai i tō kōtaha kia haere tonu.", "selectOrganization": "Kōwhiria tētahi rōpū", diff --git a/apps/mobile/src/i18n/locales/mk.json b/apps/mobile/src/i18n/locales/mk.json index 7568897d2aa..742334c3540 100644 --- a/apps/mobile/src/i18n/locales/mk.json +++ b/apps/mobile/src/i18n/locales/mk.json @@ -2975,6 +2975,7 @@ "backToProfile": "Назад на профилот", "loadErrorMessage": "Провери ја врската и обиди се повторно.", "loadErrorTitle": "Не успеа вчитувањето на твоите организации.", + "membershipLostNotice": "Веќе немаш пристап до оваа организација. Префрлено на {{target}}.", "organizationUnavailable": "Организацијата не е достапна.", "selectDescription": "Избери организација од твојот профил за да продолжиш.", "selectOrganization": "Избор на организација", diff --git a/apps/mobile/src/i18n/locales/ml.json b/apps/mobile/src/i18n/locales/ml.json index 5af7ef968ce..310be97748a 100644 --- a/apps/mobile/src/i18n/locales/ml.json +++ b/apps/mobile/src/i18n/locales/ml.json @@ -2975,6 +2975,7 @@ "backToProfile": "പ്രൊഫൈലിലേക്ക് മടങ്ങുക", "loadErrorMessage": "ഇന്റർനെറ്റ് കണക്ഷൻ പരിശോധിച്ച് വീണ്ടും ശ്രമിക്കുക.", "loadErrorTitle": "നിങ്ങളുടെ ഓർഗനൈസേഷനുകൾ ലോഡ് ചെയ്യാൻ കഴിഞ്ഞില്ല.", + "membershipLostNotice": "ഈ ഓർഗനൈസേഷനിൽ നിങ്ങൾക്ക് ഇനി പ്രവേശനമില്ല. {{target}}-ലേക്ക് മാറി.", "organizationUnavailable": "ഓർഗനൈസേഷൻ ലഭ്യമല്ല.", "selectDescription": "തുടരാൻ നിങ്ങളുടെ പ്രൊഫൈലിൽ നിന്ന് ഒരു ഓർഗനൈസേഷൻ തിരഞ്ഞെടുക്കുക.", "selectOrganization": "ഒരു ഓർഗനൈസേഷൻ തിരഞ്ഞെടുക്കുക", diff --git a/apps/mobile/src/i18n/locales/mn.json b/apps/mobile/src/i18n/locales/mn.json index 7a8c001fc5c..fef84ede33a 100644 --- a/apps/mobile/src/i18n/locales/mn.json +++ b/apps/mobile/src/i18n/locales/mn.json @@ -2975,6 +2975,7 @@ "backToProfile": "Профайл руу буцах", "loadErrorMessage": "Холболтоо шалгаад дахин оролдоно уу.", "loadErrorTitle": "Таны байгууллагуудыг ачаалж чадсангүй.", + "membershipLostNotice": "Та энэ байгууллагад хандах эрхгүй боллоо. {{target}} руу шилжлээ.", "organizationUnavailable": "Байгууллагад хандах боломжгүй.", "selectDescription": "Үргэлжлүүлэхийн тулд профайлаасаа байгууллага сонгоно уу.", "selectOrganization": "Байгууллага сонгох", diff --git a/apps/mobile/src/i18n/locales/mr.json b/apps/mobile/src/i18n/locales/mr.json index 7ec8c6bf653..fab2084f8c7 100644 --- a/apps/mobile/src/i18n/locales/mr.json +++ b/apps/mobile/src/i18n/locales/mr.json @@ -2975,6 +2975,7 @@ "backToProfile": "प्रोफाइलवर परत जा", "loadErrorMessage": "इंटरनेट जोडणी तपासा आणि पुन्हा प्रयत्न करा.", "loadErrorTitle": "तुमच्या संस्था लोड करता आल्या नाहीत.", + "membershipLostNotice": "तुम्हाला या संस्थेमध्ये आता प्रवेश नाही. {{target}} वर स्विच केले.", "organizationUnavailable": "संस्था उपलब्ध नाही.", "selectDescription": "पुढे जाण्यासाठी तुमच्या प्रोफाइलमधून संस्था निवडा.", "selectOrganization": "संस्था निवडा", diff --git a/apps/mobile/src/i18n/locales/ms.json b/apps/mobile/src/i18n/locales/ms.json index 8d70745b29f..9d2a54a3147 100644 --- a/apps/mobile/src/i18n/locales/ms.json +++ b/apps/mobile/src/i18n/locales/ms.json @@ -2975,6 +2975,7 @@ "backToProfile": "Kembali ke profil", "loadErrorMessage": "Semak sambungan anda dan cuba lagi.", "loadErrorTitle": "Tidak dapat memuatkan organisasi anda.", + "membershipLostNotice": "Anda tidak lagi mempunyai akses kepada organisasi ini. Ditukar kepada {{target}}.", "organizationUnavailable": "Organisasi tidak tersedia.", "selectDescription": "Pilih organisasi daripada profil anda untuk meneruskan.", "selectOrganization": "Pilih organisasi", diff --git a/apps/mobile/src/i18n/locales/mt.json b/apps/mobile/src/i18n/locales/mt.json index 873bd91b734..db7b7a9e49a 100644 --- a/apps/mobile/src/i18n/locales/mt.json +++ b/apps/mobile/src/i18n/locales/mt.json @@ -3095,6 +3095,7 @@ "backToProfile": "Lura għall-profil", "loadErrorMessage": "Iċċekkja l-konnessjoni tiegħek u erġa' pprova.", "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}}.", "organizationUnavailable": "L-organizzazzjoni mhix disponibbli.", "selectDescription": "Agħżel organizzazzjoni mill-profil tiegħek biex tkompli.", "selectOrganization": "Agħżel organizzazzjoni", diff --git a/apps/mobile/src/i18n/locales/my.json b/apps/mobile/src/i18n/locales/my.json index 4bed0821fd2..216ae54be91 100644 --- a/apps/mobile/src/i18n/locales/my.json +++ b/apps/mobile/src/i18n/locales/my.json @@ -2975,6 +2975,7 @@ "backToProfile": "ကိုယ်ရေးအချက်အလက်သို့ ပြန်သွားပါ", "loadErrorMessage": "အင်တာနက်ချိတ်ဆက်မှုကို စစ်ဆေးပြီး ထပ်စမ်းပါ။", "loadErrorTitle": "သင့်အဖွဲ့အစည်းများကို ရယူ၍မရပါ။", + "membershipLostNotice": "ဤအဖွဲ့အစည်းသို့ ဝင်ရောက်ခွင့် မရှိတော့ပါ။ {{target}} သို့ ပြောင်းလဲခဲ့သည်။", "organizationUnavailable": "အဖွဲ့အစည်းကို ဝင်ရောက်၍မရပါ။", "selectDescription": "ဆက်လုပ်ရန် ကိုယ်ရေးအချက်အလက်မှ အဖွဲ့အစည်းတစ်ခုကို ရွေးပါ။", "selectOrganization": "အဖွဲ့အစည်းတစ်ခု ရွေးပါ", diff --git a/apps/mobile/src/i18n/locales/nb.json b/apps/mobile/src/i18n/locales/nb.json index b7013e0bcd1..ba698bb8ca2 100644 --- a/apps/mobile/src/i18n/locales/nb.json +++ b/apps/mobile/src/i18n/locales/nb.json @@ -2975,6 +2975,7 @@ "backToProfile": "Tilbake til profil", "loadErrorMessage": "Sjekk tilkoblingen og prøv igjen.", "loadErrorTitle": "Kunne ikke laste inn organisasjonene dine.", + "membershipLostNotice": "Du har ikke lenger tilgang til denne organisasjonen. Byttet til {{target}}.", "organizationUnavailable": "Organisasjonen er ikke tilgjengelig.", "selectDescription": "Velg en organisasjon fra profilen din for å fortsette.", "selectOrganization": "Velg en organisasjon", diff --git a/apps/mobile/src/i18n/locales/ne.json b/apps/mobile/src/i18n/locales/ne.json index d7a201493b7..667312ec3fd 100644 --- a/apps/mobile/src/i18n/locales/ne.json +++ b/apps/mobile/src/i18n/locales/ne.json @@ -2975,6 +2975,7 @@ "backToProfile": "प्रोफाइलमा फर्कनुहोस्", "loadErrorMessage": "इन्टरनेट जडान जाँचेर फेरि प्रयास गर्नुहोस्।", "loadErrorTitle": "तपाईंका संस्थाहरू लोड गर्न सकिएन।", + "membershipLostNotice": "तपाईंसँग यो संस्थामा अब पहुँच छैन। {{target}} मा स्विच गरियो।", "organizationUnavailable": "संस्था उपलब्ध छैन।", "selectDescription": "जारी राख्न आफ्नो प्रोफाइलबाट एउटा संस्था छान्नुहोस्।", "selectOrganization": "संस्था चयन गर्नुहोस्", diff --git a/apps/mobile/src/i18n/locales/nl.json b/apps/mobile/src/i18n/locales/nl.json index 5688685c1a7..689e1087ea1 100644 --- a/apps/mobile/src/i18n/locales/nl.json +++ b/apps/mobile/src/i18n/locales/nl.json @@ -2967,6 +2967,7 @@ "backToProfile": "Terug naar profiel", "loadErrorMessage": "Controleer je verbinding en probeer het opnieuw.", "loadErrorTitle": "Je organisaties konden niet worden geladen.", + "membershipLostNotice": "Je hebt geen toegang meer tot deze organisatie. Overgeschakeld naar {{target}}.", "organizationUnavailable": "De organisatie is niet beschikbaar.", "selectDescription": "Kies een organisatie uit je profiel om door te gaan.", "selectOrganization": "Een organisatie selecteren", diff --git a/apps/mobile/src/i18n/locales/om.json b/apps/mobile/src/i18n/locales/om.json index 155a6566407..bea719248be 100644 --- a/apps/mobile/src/i18n/locales/om.json +++ b/apps/mobile/src/i18n/locales/om.json @@ -2975,6 +2975,7 @@ "backToProfile": "Gara piroofaayiliitti deebi'i", "loadErrorMessage": "Walitti hidhamiinsa kee ilaalii irra deebi'ii yaali.", "loadErrorTitle": "Dhaabbilee kee fe'uu hin danda'amne.", + "membershipLostNotice": "Dhaabbata kana bira ga'uuf kana booda hayyama hin qabdu. Gara {{target}} tti jijjiirame.", "organizationUnavailable": "Dhaabbanni hin argamu.", "selectDescription": "Itti fufuuf piroofaayilii kee irraa dhaabbata filadhu.", "selectOrganization": "Dhaabbata filadhu", diff --git a/apps/mobile/src/i18n/locales/or.json b/apps/mobile/src/i18n/locales/or.json index 77f4ea8f6f8..2c6a6a11886 100644 --- a/apps/mobile/src/i18n/locales/or.json +++ b/apps/mobile/src/i18n/locales/or.json @@ -2975,6 +2975,7 @@ "backToProfile": "ପ୍ରୋଫାଇଲକୁ ଫେରନ୍ତୁ", "loadErrorMessage": "ଆପଣଙ୍କ ସଂଯୋଗ ଯାଞ୍ଚ କରି ପୁଣି ଚେଷ୍ଟା କରନ୍ତୁ।", "loadErrorTitle": "ଆପଣଙ୍କ ସଂଗଠନଗୁଡ଼ିକ ଲୋଡ୍ ହୋଇପାରିଲା ନାହିଁ।", + "membershipLostNotice": "ଆପଣଙ୍କର ଏହି ସଂଗଠନରେ ଆଉ ପ୍ରବେଶ ଅଧିକାର ନାହିଁ। {{target}} କୁ ସ୍ୱିଚ୍ ହୋଇଛି।", "organizationUnavailable": "ସଂଗଠନ ଉପଲବ୍ଧ ନାହିଁ।", "selectDescription": "ଆଗକୁ ବଢ଼ିବାକୁ ଆପଣଙ୍କ ପ୍ରୋଫାଇଲରୁ ଏକ ସଂଗଠନ ବାଛନ୍ତୁ।", "selectOrganization": "ଏକ ସଂଗଠନ ବାଛନ୍ତୁ", diff --git a/apps/mobile/src/i18n/locales/pa.json b/apps/mobile/src/i18n/locales/pa.json index ae99401f258..99eed1e744d 100644 --- a/apps/mobile/src/i18n/locales/pa.json +++ b/apps/mobile/src/i18n/locales/pa.json @@ -2975,6 +2975,7 @@ "backToProfile": "ਪ੍ਰੋਫਾਈਲ 'ਤੇ ਵਾਪਸ ਜਾਓ", "loadErrorMessage": "ਆਪਣਾ ਕਨੈਕਸ਼ਨ ਜਾਂਚੋ ਅਤੇ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ।", "loadErrorTitle": "ਤੁਹਾਡੀਆਂ ਸੰਸਥਾਵਾਂ ਲੋਡ ਨਹੀਂ ਹੋ ਸਕੀਆਂ।", + "membershipLostNotice": "ਤੁਹਾਡੀ ਇਸ ਸੰਸਥਾ ਤੱਕ ਹੁਣ ਪਹੁੰਚ ਨਹੀਂ ਹੈ। {{target}} ਤੇ ਸਵਿਚ ਹੋਇਆ।", "organizationUnavailable": "ਸੰਸਥਾ ਉਪਲਬਧ ਨਹੀਂ ਹੈ।", "selectDescription": "ਜਾਰੀ ਰੱਖਣ ਲਈ ਆਪਣੀ ਪ੍ਰੋਫਾਈਲ ਤੋਂ ਇੱਕ ਸੰਸਥਾ ਚੁਣੋ।", "selectOrganization": "ਇੱਕ ਸੰਸਥਾ ਚੁਣੋ", diff --git a/apps/mobile/src/i18n/locales/pl.json b/apps/mobile/src/i18n/locales/pl.json index dd744327994..ad4861e3b7c 100644 --- a/apps/mobile/src/i18n/locales/pl.json +++ b/apps/mobile/src/i18n/locales/pl.json @@ -3047,6 +3047,7 @@ "backToProfile": "Wróć do profilu", "loadErrorMessage": "Sprawdź połączenie i spróbuj ponownie.", "loadErrorTitle": "Nie udało się wczytać twoich organizacji.", + "membershipLostNotice": "Nie masz już dostępu do tej organizacji. Przełączono na {{target}}.", "organizationUnavailable": "Organizacja jest niedostępna.", "selectDescription": "Wybierz organizację w swoim profilu, aby kontynuować.", "selectOrganization": "Wybierz organizację", diff --git a/apps/mobile/src/i18n/locales/ps.json b/apps/mobile/src/i18n/locales/ps.json index 7d433c28709..d8790ae1ed4 100644 --- a/apps/mobile/src/i18n/locales/ps.json +++ b/apps/mobile/src/i18n/locales/ps.json @@ -2975,6 +2975,7 @@ "backToProfile": "بېرته پروفایل ته ورشئ", "loadErrorMessage": "خپله انټرنېټي اړیکه وګورئ او بیا هڅه وکړئ.", "loadErrorTitle": "ستاسو سازمانونه بار نه شول.", + "membershipLostNotice": "تاسو دې سازمان ته نور لاسرسی نه لرئ. په {{target}} بدل شو.", "organizationUnavailable": "سازمان د لاسرسي وړ نه دی.", "selectDescription": "د دوام لپاره له خپل پروفایل څخه یو سازمان غوره کړئ.", "selectOrganization": "یو سازمان غوره کړئ", diff --git a/apps/mobile/src/i18n/locales/pt-BR.json b/apps/mobile/src/i18n/locales/pt-BR.json index db52efe6da7..91b619eb47e 100644 --- a/apps/mobile/src/i18n/locales/pt-BR.json +++ b/apps/mobile/src/i18n/locales/pt-BR.json @@ -3007,6 +3007,7 @@ "backToProfile": "Voltar para o perfil", "loadErrorMessage": "Verifique sua conexão e tente novamente.", "loadErrorTitle": "Não foi possível carregar suas organizações.", + "membershipLostNotice": "Você não tem mais acesso a esta organização. Mudou para {{target}}.", "organizationUnavailable": "A organização não está disponível.", "selectDescription": "Escolha uma organização no seu perfil para continuar.", "selectOrganization": "Selecione uma organização", diff --git a/apps/mobile/src/i18n/locales/pt.json b/apps/mobile/src/i18n/locales/pt.json index 63a37702c84..7136384a27e 100644 --- a/apps/mobile/src/i18n/locales/pt.json +++ b/apps/mobile/src/i18n/locales/pt.json @@ -3015,6 +3015,7 @@ "backToProfile": "Voltar ao perfil", "loadErrorMessage": "Verifica a tua ligação e tenta novamente.", "loadErrorTitle": "Não foi possível carregar as suas organizações.", + "membershipLostNotice": "Já não tem acesso a esta organização. Mudou para {{target}}.", "organizationUnavailable": "A organização não está disponível.", "selectDescription": "Escolha uma organização no seu perfil para continuar.", "selectOrganization": "Selecionar uma organização", diff --git a/apps/mobile/src/i18n/locales/ro.json b/apps/mobile/src/i18n/locales/ro.json index 4d2b1eadf8b..3bca3caf5bb 100644 --- a/apps/mobile/src/i18n/locales/ro.json +++ b/apps/mobile/src/i18n/locales/ro.json @@ -3015,6 +3015,7 @@ "backToProfile": "Înapoi la profil", "loadErrorMessage": "Verifică conexiunea și încearcă din nou.", "loadErrorTitle": "Nu s-au putut încărca organizațiile tale.", + "membershipLostNotice": "Nu mai ai acces la această organizație. S-a comutat la {{target}}.", "organizationUnavailable": "Organizația nu este disponibilă.", "selectDescription": "Alege o organizație din profilul tău pentru a continua.", "selectOrganization": "Selectează o organizație", diff --git a/apps/mobile/src/i18n/locales/ru.json b/apps/mobile/src/i18n/locales/ru.json index 2384ac046ec..d68e358dfd0 100644 --- a/apps/mobile/src/i18n/locales/ru.json +++ b/apps/mobile/src/i18n/locales/ru.json @@ -3047,6 +3047,7 @@ "backToProfile": "К профилю", "loadErrorMessage": "Проверьте подключение и попробуйте снова.", "loadErrorTitle": "Не удалось загрузить список ваших организаций.", + "membershipLostNotice": "У вас больше нет доступа к этой организации. Переключено на {{target}}.", "organizationUnavailable": "Организация недоступна.", "selectDescription": "Выберите организацию в профиле, чтобы продолжить.", "selectOrganization": "Выберите организацию", diff --git a/apps/mobile/src/i18n/locales/si.json b/apps/mobile/src/i18n/locales/si.json index 1699e6290a3..71585848082 100644 --- a/apps/mobile/src/i18n/locales/si.json +++ b/apps/mobile/src/i18n/locales/si.json @@ -2975,6 +2975,7 @@ "backToProfile": "පැතිකඩට ආපසු යන්න", "loadErrorMessage": "ඔබේ අන්තර්ජාල සම්බන්ධතාව පරීක්ෂා කර නැවත උත්සාහ කරන්න.", "loadErrorTitle": "ඔබේ සංවිධාන පූරණය කළ නොහැකි විය.", + "membershipLostNotice": "ඔබට මෙම සංවිධානයට තවදුරටත් ප්‍රවේශ වීමට අවසර නොමැත. {{target}} වෙත මාරු විය.", "organizationUnavailable": "සංවිධානය ලබා ගත නොහැක.", "selectDescription": "ඉදිරියට යාමට ඔබේ පැතිකඩෙන් සංවිධානයක් තෝරන්න.", "selectOrganization": "සංවිධානයක් තෝරන්න", diff --git a/apps/mobile/src/i18n/locales/sk.json b/apps/mobile/src/i18n/locales/sk.json index 4d77dd8deb5..655a4acf6ec 100644 --- a/apps/mobile/src/i18n/locales/sk.json +++ b/apps/mobile/src/i18n/locales/sk.json @@ -3055,6 +3055,7 @@ "backToProfile": "Späť na profil", "loadErrorMessage": "Skontroluj pripojenie a skús to znova.", "loadErrorTitle": "Tvoje organizácie sa nepodarilo načítať.", + "membershipLostNotice": "Už nemáš prístup k tejto organizácii. Prepnuté na {{target}}.", "organizationUnavailable": "Organizácia nie je dostupná.", "selectDescription": "Ak chceš pokračovať, vyber organizáciu vo svojom profile.", "selectOrganization": "Vybrať organizáciu", diff --git a/apps/mobile/src/i18n/locales/sl.json b/apps/mobile/src/i18n/locales/sl.json index c1baf722585..4413ec14427 100644 --- a/apps/mobile/src/i18n/locales/sl.json +++ b/apps/mobile/src/i18n/locales/sl.json @@ -3055,6 +3055,7 @@ "backToProfile": "Nazaj na profil", "loadErrorMessage": "Preveri povezavo in poskusi znova.", "loadErrorTitle": "Tvojih organizacij ni bilo mogoče naložiti.", + "membershipLostNotice": "Nimaš več dostopa do te organizacije. Preklopljeno na {{target}}.", "organizationUnavailable": "Organizacija ni na voljo.", "selectDescription": "Za nadaljevanje na svojem profilu izberi organizacijo.", "selectOrganization": "Izberi organizacijo", diff --git a/apps/mobile/src/i18n/locales/so.json b/apps/mobile/src/i18n/locales/so.json index 2986d845c80..2c4d731b221 100644 --- a/apps/mobile/src/i18n/locales/so.json +++ b/apps/mobile/src/i18n/locales/so.json @@ -2975,6 +2975,7 @@ "backToProfile": "Ku noqo bogga shakhsiga", "loadErrorMessage": "Hubi xiriirkaaga internetka oo mar kale isku day.", "loadErrorTitle": "Ururradaada lama soo shubi karin.", + "membershipLostNotice": "Mar dambe ma lihid oggolaansho aad ku gasho ururkan. Waxaa lagu beddelay {{target}}.", "organizationUnavailable": "Ururka lama heli karo.", "selectDescription": "Boggaaga shakhsiga ka dooro urur si aad u sii socoto.", "selectOrganization": "Dooro urur", diff --git a/apps/mobile/src/i18n/locales/sq.json b/apps/mobile/src/i18n/locales/sq.json index b4c7c62676f..cfc5d96d10f 100644 --- a/apps/mobile/src/i18n/locales/sq.json +++ b/apps/mobile/src/i18n/locales/sq.json @@ -2975,6 +2975,7 @@ "backToProfile": "Kthehu te profili", "loadErrorMessage": "Kontrollo lidhjen dhe provo përsëri.", "loadErrorTitle": "Nuk u ngarkuan organizatat e tua.", + "membershipLostNotice": "Nuk ke më qasje në këtë organizatë. U kalua te {{target}}.", "organizationUnavailable": "Organizata nuk është e disponueshme.", "selectDescription": "Zgjidh një organizatë nga profili yt për të vazhduar.", "selectOrganization": "Zgjidh një organizatë", diff --git a/apps/mobile/src/i18n/locales/sr.json b/apps/mobile/src/i18n/locales/sr.json index 77ec26b8d52..1276fcbb06a 100644 --- a/apps/mobile/src/i18n/locales/sr.json +++ b/apps/mobile/src/i18n/locales/sr.json @@ -3015,6 +3015,7 @@ "backToProfile": "Nazad na profil", "loadErrorMessage": "Proveri vezu i pokušaj ponovo.", "loadErrorTitle": "Učitavanje tvojih organizacija nije uspelo.", + "membershipLostNotice": "Više nemaš pristup ovoj organizaciji. Prebačeno na {{target}}.", "organizationUnavailable": "Organizacija nije dostupna.", "selectDescription": "Izaberi organizaciju na svom profilu da nastaviš.", "selectOrganization": "Izaberi organizaciju", diff --git a/apps/mobile/src/i18n/locales/sv.json b/apps/mobile/src/i18n/locales/sv.json index b6a2c421500..77c6ede5200 100644 --- a/apps/mobile/src/i18n/locales/sv.json +++ b/apps/mobile/src/i18n/locales/sv.json @@ -2975,6 +2975,7 @@ "backToProfile": "Tillbaka till profilen", "loadErrorMessage": "Kontrollera anslutningen och försök igen.", "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}}.", "organizationUnavailable": "Organisationen är inte tillgänglig.", "selectDescription": "Välj en organisation i din profil för att fortsätta.", "selectOrganization": "Välj en organisation", diff --git a/apps/mobile/src/i18n/locales/sw.json b/apps/mobile/src/i18n/locales/sw.json index 2b5c2f52096..14ac7ec2347 100644 --- a/apps/mobile/src/i18n/locales/sw.json +++ b/apps/mobile/src/i18n/locales/sw.json @@ -2975,6 +2975,7 @@ "backToProfile": "Rudi kwenye wasifu", "loadErrorMessage": "Angalia muunganisho wako wa intaneti na ujaribu tena.", "loadErrorTitle": "Imeshindikana kupakia mashirika yako.", + "membershipLostNotice": "Huna tena ruhusa ya kufikia shirika hili. Imebadilishwa kwenda {{target}}.", "organizationUnavailable": "Shirika halipatikani.", "selectDescription": "Chagua shirika kutoka kwenye wasifu wako ili uendelee.", "selectOrganization": "Chagua shirika", diff --git a/apps/mobile/src/i18n/locales/ta.json b/apps/mobile/src/i18n/locales/ta.json index 19a9336d6fc..27a39c19391 100644 --- a/apps/mobile/src/i18n/locales/ta.json +++ b/apps/mobile/src/i18n/locales/ta.json @@ -2975,6 +2975,7 @@ "backToProfile": "சுயவிவரத்திற்குத் திரும்பவும்", "loadErrorMessage": "உங்கள் இணைய இணைப்பைச் சரிபார்த்து மீண்டும் முயற்சிக்கவும்.", "loadErrorTitle": "உங்கள் நிறுவனங்களை ஏற்ற முடியவில்லை.", + "membershipLostNotice": "இந்த நிறுவனத்தை இனி அணுக உங்களுக்கு அனுமதி இல்லை. {{target}}-க்கு மாற்றப்பட்டது.", "organizationUnavailable": "நிறுவனம் கிடைக்கவில்லை.", "selectDescription": "தொடர உங்கள் சுயவிவரத்திலிருந்து ஒரு நிறுவனத்தைத் தேர்ந்தெடுக்கவும்.", "selectOrganization": "ஒரு நிறுவனத்தைத் தேர்ந்தெடுக்கவும்", diff --git a/apps/mobile/src/i18n/locales/te.json b/apps/mobile/src/i18n/locales/te.json index b52adbb5f71..c930d44f342 100644 --- a/apps/mobile/src/i18n/locales/te.json +++ b/apps/mobile/src/i18n/locales/te.json @@ -2975,6 +2975,7 @@ "backToProfile": "ప్రొఫైల్‌కు తిరిగి వెళ్లండి", "loadErrorMessage": "మీ కనెక్షన్‌ను తనిఖీ చేసి మళ్లీ ప్రయత్నించండి.", "loadErrorTitle": "మీ సంస్థలను లోడ్ చేయలేకపోయాము.", + "membershipLostNotice": "ఈ సంస్థను ఇక యాక్సెస్ చేయడానికి మీకు అనుమతి లేదు. {{target}}కి మార్చబడింది.", "organizationUnavailable": "సంస్థ అందుబాటులో లేదు.", "selectDescription": "కొనసాగించడానికి మీ ప్రొఫైల్‌లో ఒక సంస్థను ఎంచుకోండి.", "selectOrganization": "ఒక సంస్థను ఎంచుకోండి", diff --git a/apps/mobile/src/i18n/locales/th.json b/apps/mobile/src/i18n/locales/th.json index 23a762b1e50..808dbefa752 100644 --- a/apps/mobile/src/i18n/locales/th.json +++ b/apps/mobile/src/i18n/locales/th.json @@ -2975,6 +2975,7 @@ "backToProfile": "กลับไปยังโปรไฟล์", "loadErrorMessage": "ตรวจสอบการเชื่อมต่อแล้วลองอีกครั้ง", "loadErrorTitle": "โหลดองค์กรของคุณไม่สำเร็จ", + "membershipLostNotice": "คุณไม่มีสิทธิ์เข้าถึงองค์กรนี้อีกต่อไป เปลี่ยนเป็น {{target}} แล้ว", "organizationUnavailable": "องค์กรไม่พร้อมใช้งาน", "selectDescription": "เลือกองค์กรจากโปรไฟล์ของคุณเพื่อดำเนินการต่อ", "selectOrganization": "เลือกองค์กร", diff --git a/apps/mobile/src/i18n/locales/tr.json b/apps/mobile/src/i18n/locales/tr.json index b015f914f64..04fee80f631 100644 --- a/apps/mobile/src/i18n/locales/tr.json +++ b/apps/mobile/src/i18n/locales/tr.json @@ -2967,6 +2967,7 @@ "backToProfile": "Profile geri dön", "loadErrorMessage": "Bağlantını kontrol edip tekrar dene.", "loadErrorTitle": "Kuruluşların yüklenemedi.", + "membershipLostNotice": "Bu kuruluşa artık erişimin yok. {{target}}'e geçildi.", "organizationUnavailable": "Kuruluş kullanılamıyor.", "selectDescription": "Devam etmek için profilinden bir kuruluş seç.", "selectOrganization": "Bir kuruluş seç", diff --git a/apps/mobile/src/i18n/locales/uk.json b/apps/mobile/src/i18n/locales/uk.json index aaf49134225..3f1bbcdf5ba 100644 --- a/apps/mobile/src/i18n/locales/uk.json +++ b/apps/mobile/src/i18n/locales/uk.json @@ -3047,6 +3047,7 @@ "backToProfile": "Назад до профілю", "loadErrorMessage": "Перевір з’єднання та спробуй ще раз.", "loadErrorTitle": "Не вдалося завантажити ваші організації.", + "membershipLostNotice": "У вас більше немає доступу до цієї організації. Перемкнуто на {{target}}.", "organizationUnavailable": "Організація недоступна.", "selectDescription": "Виберіть організацію у своєму профілі, щоб продовжити.", "selectOrganization": "Виберіть організацію", diff --git a/apps/mobile/src/i18n/locales/ur.json b/apps/mobile/src/i18n/locales/ur.json index d8b70dc05aa..d7a01f83d0c 100644 --- a/apps/mobile/src/i18n/locales/ur.json +++ b/apps/mobile/src/i18n/locales/ur.json @@ -2975,6 +2975,7 @@ "backToProfile": "پروفائل پر واپس جائیں", "loadErrorMessage": "اپنا انٹرنیٹ کنکشن چیک کریں اور دوبارہ کوشش کریں۔", "loadErrorTitle": "آپ کی تنظیمیں لوڈ نہیں ہو سکیں۔", + "membershipLostNotice": "آپ کو اب اس تنظیم تک رسائی حاصل نہیں ہے۔ {{target}} پر منتقل کر دیا گیا۔", "organizationUnavailable": "تنظیم دستیاب نہیں ہے۔", "selectDescription": "جاری رکھنے کے لیے اپنے پروفائل سے ایک تنظیم منتخب کریں۔", "selectOrganization": "ایک تنظیم منتخب کریں", diff --git a/apps/mobile/src/i18n/locales/uz.json b/apps/mobile/src/i18n/locales/uz.json index 155c709ed33..cc1cf66f283 100644 --- a/apps/mobile/src/i18n/locales/uz.json +++ b/apps/mobile/src/i18n/locales/uz.json @@ -2975,6 +2975,7 @@ "backToProfile": "Profilga qaytish", "loadErrorMessage": "Internetga ulanishni tekshiring va qayta urinib ko‘ring.", "loadErrorTitle": "Tashkilotlaringizni yuklab bo'lmadi.", + "membershipLostNotice": "Sizda ushbu tashkilotga kirish huquqi endi yo'q. {{target}} ga o'tkazildi.", "organizationUnavailable": "Tashkilot mavjud emas.", "selectDescription": "Davom etish uchun profilingizdan tashkilotni tanlang.", "selectOrganization": "Tashkilotni tanlang", diff --git a/apps/mobile/src/i18n/locales/vi.json b/apps/mobile/src/i18n/locales/vi.json index 15c6f16140b..42ac245076e 100644 --- a/apps/mobile/src/i18n/locales/vi.json +++ b/apps/mobile/src/i18n/locales/vi.json @@ -2967,6 +2967,7 @@ "backToProfile": "Quay lại hồ sơ", "loadErrorMessage": "Kiểm tra kết nối và thử lại.", "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}}.", "organizationUnavailable": "Không thể truy cập tổ chức.", "selectDescription": "Chọn một tổ chức từ hồ sơ của bạn để tiếp tục.", "selectOrganization": "Chọn một tổ chức", diff --git a/apps/mobile/src/i18n/locales/yo.json b/apps/mobile/src/i18n/locales/yo.json index 2e0f31c9319..7c6f9fdb978 100644 --- a/apps/mobile/src/i18n/locales/yo.json +++ b/apps/mobile/src/i18n/locales/yo.json @@ -2975,6 +2975,7 @@ "backToProfile": "Padà sí prófáìlì", "loadErrorMessage": "Ṣàyẹ̀wò ìsopọ̀ rẹ kí o tún gbìyànjú.", "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}}.", "organizationUnavailable": "Àjọ náà kò sí lárọ̀ọ́wọ́tó.", "selectDescription": "Yan àjọ kan láti prófáìlì rẹ láti tẹ̀síwájú.", "selectOrganization": "Yan àjọ kan", diff --git a/apps/mobile/src/i18n/locales/zh-Hans.json b/apps/mobile/src/i18n/locales/zh-Hans.json index 6af023f9ab5..a12721d31cb 100644 --- a/apps/mobile/src/i18n/locales/zh-Hans.json +++ b/apps/mobile/src/i18n/locales/zh-Hans.json @@ -2967,6 +2967,7 @@ "backToProfile": "返回个人资料", "loadErrorMessage": "请检查网络连接后重试。", "loadErrorTitle": "无法加载你的组织列表。", + "membershipLostNotice": "你已无法访问此组织。已切换至 {{target}}。", "organizationUnavailable": "此组织不可用。", "selectDescription": "请前往个人资料页面,选择一个组织后继续。", "selectOrganization": "选择组织", diff --git a/apps/mobile/src/i18n/locales/zh-Hant.json b/apps/mobile/src/i18n/locales/zh-Hant.json index f4fe45a3b1c..90d00f21396 100644 --- a/apps/mobile/src/i18n/locales/zh-Hant.json +++ b/apps/mobile/src/i18n/locales/zh-Hant.json @@ -2967,6 +2967,7 @@ "backToProfile": "返回個人資料", "loadErrorMessage": "請檢查網路連線後再試一次。", "loadErrorTitle": "無法載入你的組織清單。", + "membershipLostNotice": "你已無法存取此組織。已切換至 {{target}}。", "organizationUnavailable": "無法存取組織。", "selectDescription": "請到個人資料頁面選取組織,即可繼續。", "selectOrganization": "選取組織", diff --git a/apps/mobile/src/i18n/locales/zu.json b/apps/mobile/src/i18n/locales/zu.json index c5d1266abc6..f9fb505e364 100644 --- a/apps/mobile/src/i18n/locales/zu.json +++ b/apps/mobile/src/i18n/locales/zu.json @@ -2975,6 +2975,7 @@ "backToProfile": "Buyela kuphrofayili", "loadErrorMessage": "Hlola uxhumano lwakho bese uzama futhi.", "loadErrorTitle": "Asikwazanga ukulayisha izinhlangano zakho.", + "membershipLostNotice": "Awusenayo imvume yokufinyelela kule nhlangano. Ushintshelwe ku-{{target}}.", "organizationUnavailable": "Inhlangano ayitholakali.", "selectDescription": "Khetha inhlangano kuphrofayili yakho ukuze uqhubeke.", "selectOrganization": "Khetha inhlangano", From 00a9f40735515b0f1fa27ed7eb96fbcfa2b1bd9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= <4062236+iscekic@users.noreply.github.com> Date: Mon, 19 Oct 2026 15:47:00 +0000 Subject: [PATCH 6/9] fix(mobile): recover a stored selection the first fetched list omits A stored organization selection missing from the first fetched list now switches to the first remaining membership (or Personal) instead of rendering the dead selection with a generic credits name and a failed balance; the picker and the boundary surfaces then show the recovered context. A manual selection records the snapshot it was made against as its baseline, and a snapshot observed during a save or restore error is re-evaluated once the error clears. The mounted suites that pinned the old stored-missing contract update to the new one: the provider recovers before the sheet or card renders, so the staged selections now seed the fetched list they are asserted against. The four suites that reach react-native's Flow source through the toast adapter mock the adapter at that edge. --- .../context-control.mounted.test.tsx | 54 ++++++++++++++----- .../profile-credits-card.mounted.test.tsx | 47 +++++++++++++--- .../src/lib/deep-link-launch.mounted.test.tsx | 16 +++++- .../lib/organization-context.mounted.test.tsx | 16 +++++- apps/mobile/src/lib/organization-context.tsx | 25 ++++----- 5 files changed, 122 insertions(+), 36 deletions(-) diff --git a/apps/mobile/src/components/context-control.mounted.test.tsx b/apps/mobile/src/components/context-control.mounted.test.tsx index a116c3255e2..4ebbc4eb40e 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', () => ({ @@ -241,7 +249,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 }) => { @@ -260,13 +271,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 () => { @@ -290,9 +306,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 }) => { @@ -305,7 +322,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]); } ); @@ -353,18 +370,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 => { @@ -463,6 +481,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', @@ -471,17 +492,22 @@ describe.each(['ios', 'android'])('ContextControl on %s', os => { const ui = await mount({ 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({ 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/profile-credits-card.mounted.test.tsx b/apps/mobile/src/components/profile-credits-card.mounted.test.tsx index ed402ec0621..8ccf0f69ae1 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, @@ -256,6 +270,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(); @@ -291,16 +308,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); @@ -318,6 +342,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); @@ -566,6 +594,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); @@ -587,6 +618,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/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..275e708edaf 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); }); @@ -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.tsx b/apps/mobile/src/lib/organization-context.tsx index 345c25c441a..185c7b519eb 100644 --- a/apps/mobile/src/lib/organization-context.tsx +++ b/apps/mobile/src/lib/organization-context.tsx @@ -226,15 +226,17 @@ export function OrganizationProvider({ children }: { readonly children: ReactNod persist, ]); - // Detection: a settled, successful snapshot that no longer names the stored + // Detection: a settled, successful snapshot that does not name the stored // organization arms the recovery effect (declared below `setOrganizationId`, - // which it calls). A pick records the snapshot it was made against (above); - // a restored selection has no baseline, so its very first snapshot is - // evaluated — a cold start whose list already misses the stored org recovers - // instead of trusting the dead selection. Personal (null) and the default - // resolution (above) need no detection. The baseline updates only after the - // guards pass, so a snapshot observed during an error is re-evaluated once - // the error clears. + // 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; @@ -284,10 +286,9 @@ export function OrganizationProvider({ children }: { readonly children: ReactNod const setOrganizationId = useCallback( (id: string | null) => { - // A manual selection disarms the recovery switch: the user moved first. - // The picker only offers orgs from the current snapshot, so that - // snapshot is the selection's baseline; the next snapshot missing the - // selection means the membership vanished after the pick. + // 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 From 7443c669b8650062379ffd8762ee7b629c11d668 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= <4062236+iscekic@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:50:39 +0000 Subject: [PATCH 7/9] test(mobile): seed the fetched list in the organization credits expiry test The context provider recovers a stored selection that the first fetched list omits, so a test that stages an organization selection must publish that organization in the shared list; otherwise the card renders the personal credit blocks. --- apps/mobile/src/components/profile-credits-card.mounted.test.tsx | 1 + 1 file changed, 1 insertion(+) 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 0a5bb3f6f4f..577e3ce5ff2 100644 --- a/apps/mobile/src/components/profile-credits-card.mounted.test.tsx +++ b/apps/mobile/src/components/profile-credits-card.mounted.test.tsx @@ -348,6 +348,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 { From d0a74a387f9705048e0d48781ee63de774706888 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= <4062236+iscekic@users.noreply.github.com> Date: Wed, 21 Oct 2026 19:45:06 +0000 Subject: [PATCH 8/9] test(mobile): align the PR review context line with lost-membership recovery The provider now switches a stored selection that the settled memberships list no longer names, so the line shows the recovered account instead of the unavailable scope. Mock the toast adapter the provider imports, as the other provider-mounted suites do. --- .../pr-review-context-line.mounted.test.tsx | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) 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'); }); }); From ceef0e443239bc0c919ff0b81ec35e92d85dd5c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= <4062236+iscekic@users.noreply.github.com> Date: Wed, 21 Oct 2026 19:50:31 +0000 Subject: [PATCH 9/9] test(mobile): resolve the memberships list with the stored organization The restoration test asserted the stored selection survives a list that does not name it, which races the provider's lost-membership switch. Give the resolved list the stored organization so the assertion is stable. --- apps/mobile/src/lib/organization-context.mounted.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/mobile/src/lib/organization-context.mounted.test.tsx b/apps/mobile/src/lib/organization-context.mounted.test.tsx index 275e708edaf..3e1117dde57 100644 --- a/apps/mobile/src/lib/organization-context.mounted.test.tsx +++ b/apps/mobile/src/lib/organization-context.mounted.test.tsx @@ -146,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'); });