From 1908a33a9fdd53adb92be61243eff030b07804c3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 19 Apr 2026 18:49:06 -0700 Subject: [PATCH 1/3] chore: bump mobile app version to 2.9.17 (#1993) Update build numbers, platform build files, and deployment timestamps after successful deployment. Co-authored-by: github-actions[bot] --- app/android/app/build.gradle | 2 +- app/version.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/android/app/build.gradle b/app/android/app/build.gradle index c51292a48..cefbe704b 100644 --- a/app/android/app/build.gradle +++ b/app/android/app/build.gradle @@ -132,7 +132,7 @@ android { applicationId "com.proofofpassportapp" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 148 + versionCode 149 versionName "2.9.17" manifestPlaceholders = [appAuthRedirectScheme: 'com.proofofpassportapp'] externalNativeBuild { diff --git a/app/version.json b/app/version.json index 7335f7191..ee3e922e3 100644 --- a/app/version.json +++ b/app/version.json @@ -4,7 +4,7 @@ "lastDeployed": "2026-04-14T21:02:24.000Z" }, "android": { - "build": 148, - "lastDeployed": "2026-04-14T21:02:24.000Z" + "build": 149, + "lastDeployed": "2026-04-20T00:22:24.761Z" } } From d6e18ca853f160ce8642fd5054546bb188798e72 Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Sun, 19 Apr 2026 18:54:43 -0700 Subject: [PATCH 2/3] fix: address PR 1991 review comments on points/gratification flow (#1994) * fixes * pr feedback --- app/src/screens/app/GratificationScreen.tsx | 13 ++- .../verification/ProofRequestStatusScreen.tsx | 50 +++++++--- .../services/logging/logger/lokiTransport.ts | 3 +- .../src/screens/GratificationScreen.test.tsx | 29 ++---- .../ProofRequestStatusScreen.test.tsx | 92 ++++++++++++++++++- 5 files changed, 142 insertions(+), 45 deletions(-) diff --git a/app/src/screens/app/GratificationScreen.tsx b/app/src/screens/app/GratificationScreen.tsx index b780a1d96..0348e47aa 100644 --- a/app/src/screens/app/GratificationScreen.tsx +++ b/app/src/screens/app/GratificationScreen.tsx @@ -11,7 +11,8 @@ import { } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { Text, View, YStack } from 'tamagui'; -import { useNavigation, useRoute } from '@react-navigation/native'; +import type { StaticScreenProps } from '@react-navigation/native'; +import { useNavigation } from '@react-navigation/native'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { X } from '@tamagui/lucide-icons'; @@ -30,13 +31,15 @@ import SelfLogo from '@/assets/logos/self.svg'; import { SystemBars } from '@/components/SystemBars'; import type { RootStackParamList } from '@/navigation'; -const GratificationScreen: React.FC = () => { +type GratificationScreenProps = StaticScreenProps<{ + points?: number; +}>; + +const GratificationScreen: React.FC = ({ route }) => { const { top, bottom } = useSafeAreaInsets(); const navigation = useNavigation>(); - const route = useRoute(); - const params = route.params as { points?: number } | undefined; - const pointsEarned = params?.points ?? 0; + const pointsEarned = route.params?.points ?? 0; const [isAnimationFinished, setIsAnimationFinished] = useState(false); const { width: screenWidth, height: screenHeight } = Dimensions.get('window'); diff --git a/app/src/screens/verification/ProofRequestStatusScreen.tsx b/app/src/screens/verification/ProofRequestStatusScreen.tsx index 904d12a25..f8d5e327b 100644 --- a/app/src/screens/verification/ProofRequestStatusScreen.tsx +++ b/app/src/screens/verification/ProofRequestStatusScreen.tsx @@ -31,10 +31,16 @@ import { } from '@/integrations/haptics'; import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout'; import type { RootStackParamList } from '@/navigation'; +import { + hasUserAnIdentityDocumentRegistered, + hasUserDoneThePointsDisclosure, +} from '@/services/points'; import { getWhiteListedDisclosureAddresses } from '@/services/points/utils'; import { useProofHistoryStore } from '@/stores/proofHistoryStore'; import { ProofStatus } from '@/stores/proofTypes'; +const PREREQ_CHECK_TIMEOUT_MS = 3000; + const SuccessScreen: React.FC = () => { const selfClient = useSelfClient(); const { trackEvent } = selfClient; @@ -68,23 +74,37 @@ const SuccessScreen: React.FC = () => { buttonTap(); const completedSessionId = sessionId; + const cleanupLater = () => { + setTimeout(() => { + if (useProvingStore.getState().uuid === completedSessionId) { + selfClient.getSelfAppState().cleanSelfApp(); + } + }, 2000); + }; + if (whitelistedPoints !== null) { - navigation.navigate('Gratification', { - points: whitelistedPoints, - }); - setTimeout(() => { - if (useProvingStore.getState().uuid === completedSessionId) { - selfClient.getSelfAppState().cleanSelfApp(); - } - }, 2000); - } else { - goHome(); - setTimeout(() => { - if (useProvingStore.getState().uuid === completedSessionId) { - selfClient.getSelfAppState().cleanSelfApp(); - } - }, 2000); + // Bound the prereq checks so a stalled network call can't trap the user + // on this screen. On timeout we fall through to goHome() — the safe + // default, since Gratification would just bounce them via the guardrail. + const timeout = new Promise(resolve => + setTimeout(() => resolve(false), PREREQ_CHECK_TIMEOUT_MS), + ); + const [hasDocument, hasDisclosed] = await Promise.all([ + Promise.race([hasUserAnIdentityDocumentRegistered(), timeout]), + Promise.race([hasUserDoneThePointsDisclosure(), timeout]), + ]); + + if (hasDocument && hasDisclosed) { + navigation.navigate('Gratification', { + points: whitelistedPoints, + }); + cleanupLater(); + return; + } } + + goHome(); + cleanupLater(); }, [ whitelistedPoints, navigation, diff --git a/app/src/services/logging/logger/lokiTransport.ts b/app/src/services/logging/logger/lokiTransport.ts index 6f85de7bc..446959ede 100644 --- a/app/src/services/logging/logger/lokiTransport.ts +++ b/app/src/services/logging/logger/lokiTransport.ts @@ -5,6 +5,7 @@ import type { AppStateStatus } from 'react-native'; import { AppState } from 'react-native'; import type { transportFunctionType } from 'react-native-logs'; +import { v4 as uuidv4 } from 'uuid'; import { registerDocumentChangeCallback } from '@/providers/passportDataProvider'; import { useSettingStore } from '@/stores/settingStore'; @@ -31,7 +32,7 @@ interface LokiPayload { } // Per-session ID for grouping logs in Grafana (not persistent, not user-identifiable) -const sessionId = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`; +const sessionId = uuidv4(); // Batch management state let batch: LokiLogEntry[] = []; diff --git a/app/tests/src/screens/GratificationScreen.test.tsx b/app/tests/src/screens/GratificationScreen.test.tsx index 41ab91d39..fd914bcce 100644 --- a/app/tests/src/screens/GratificationScreen.test.tsx +++ b/app/tests/src/screens/GratificationScreen.test.tsx @@ -2,7 +2,7 @@ // SPDX-License-Identifier: BUSL-1.1 // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. -import { useNavigation, useRoute } from '@react-navigation/native'; +import { useNavigation } from '@react-navigation/native'; import { render, waitFor } from '@testing-library/react-native'; import GratificationScreen from '@/screens/app/GratificationScreen'; @@ -48,7 +48,6 @@ jest.mock('react-native-safe-area-context', () => ({ jest.mock('@react-navigation/native', () => ({ useNavigation: jest.fn(), - useRoute: jest.fn(), })); // Mock Tamagui components to avoid theme provider requirement @@ -103,7 +102,9 @@ jest.mock('@/assets/logos/self.svg', () => 'SelfLogo'); const mockUseNavigation = useNavigation as jest.MockedFunction< typeof useNavigation >; -const mockUseRoute = useRoute as jest.MockedFunction; + +const renderScreen = (params: { points?: number } = {}) => + render(); describe('GratificationScreen', () => { const mockNavigate = jest.fn(); @@ -116,18 +117,10 @@ describe('GratificationScreen', () => { navigate: mockNavigate, goBack: mockGoBack, } as any); - - mockUseRoute.mockReturnValue({ - params: {}, - } as any); }); it('should use default points value when not provided', async () => { - mockUseRoute.mockReturnValue({ - params: {}, - } as any); - - const { getByText } = render(); + const { getByText } = renderScreen(); await waitFor(() => { expect(getByText('0')).toBeTruthy(); @@ -135,11 +128,7 @@ describe('GratificationScreen', () => { }); it('should use custom points value when provided', async () => { - mockUseRoute.mockReturnValue({ - params: { points: 50 }, - } as any); - - const { getByText } = render(); + const { getByText } = renderScreen({ points: 50 }); await waitFor(() => { expect(getByText('50')).toBeTruthy(); @@ -147,11 +136,7 @@ describe('GratificationScreen', () => { }); it('should display referral points value (24) when passed', async () => { - mockUseRoute.mockReturnValue({ - params: { points: 24 }, - } as any); - - const { getByText } = render(); + const { getByText } = renderScreen({ points: 24 }); await waitFor(() => { expect(getByText('24')).toBeTruthy(); diff --git a/app/tests/src/screens/verification/ProofRequestStatusScreen.test.tsx b/app/tests/src/screens/verification/ProofRequestStatusScreen.test.tsx index a34436dd7..7f16d9750 100644 --- a/app/tests/src/screens/verification/ProofRequestStatusScreen.test.tsx +++ b/app/tests/src/screens/verification/ProofRequestStatusScreen.test.tsx @@ -131,6 +131,11 @@ jest.mock('@/services/points/utils', () => ({ getWhiteListedDisclosureAddresses: jest.fn(), })); +jest.mock('@/services/points', () => ({ + hasUserAnIdentityDocumentRegistered: jest.fn(), + hasUserDoneThePointsDisclosure: jest.fn(), +})); + jest.mock('@/stores/proofHistoryStore', () => ({ useProofHistoryStore: jest.fn(), })); @@ -157,6 +162,11 @@ const { getWhiteListedDisclosureAddresses } = jest.requireMock( ) as { getWhiteListedDisclosureAddresses: jest.Mock; }; +const { hasUserAnIdentityDocumentRegistered, hasUserDoneThePointsDisclosure } = + jest.requireMock('@/services/points') as { + hasUserAnIdentityDocumentRegistered: jest.Mock; + hasUserDoneThePointsDisclosure: jest.Mock; + }; const { useProofHistoryStore } = jest.requireMock( '@/stores/proofHistoryStore', ) as { @@ -211,6 +221,8 @@ describe('ProofRequestStatusScreen', () => { updateProofStatus: mockUpdateProofStatus, }); getWhiteListedDisclosureAddresses.mockResolvedValue([]); + hasUserAnIdentityDocumentRegistered.mockResolvedValue(true); + hasUserDoneThePointsDisclosure.mockResolvedValue(true); const useProvingStore = Object.assign( (selector: (state: typeof provingState) => unknown) => @@ -282,8 +294,10 @@ describe('ProofRequestStatusScreen', () => { fireEvent.press(screen.getByTestId('primary-button')); - expect(mockNavigate).toHaveBeenCalledWith('Gratification', { - points: 25, + await waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith('Gratification', { + points: 25, + }); }); expect(mockGoHome).not.toHaveBeenCalled(); @@ -294,6 +308,80 @@ describe('ProofRequestStatusScreen', () => { expect(mockCleanSelfApp).toHaveBeenCalledTimes(1); }); + it('falls back to Home when whitelisted but points prerequisites are not met', async () => { + selfAppState.selfApp.endpoint = '0xABC'; + getWhiteListedDisclosureAddresses.mockResolvedValue([ + { + contract_address: '0xabc', + points_per_disclosure: 25, + }, + ]); + hasUserDoneThePointsDisclosure.mockResolvedValue(false); + + render(); + + await waitFor(() => { + expect(getWhiteListedDisclosureAddresses).toHaveBeenCalledTimes(1); + }); + + fireEvent.press(screen.getByTestId('primary-button')); + + await waitFor(() => { + expect(mockGoHome).toHaveBeenCalledTimes(1); + }); + expect(mockNavigate).not.toHaveBeenCalledWith( + 'Gratification', + expect.anything(), + ); + + act(() => { + jest.advanceTimersByTime(2000); + }); + + expect(mockCleanSelfApp).toHaveBeenCalledTimes(1); + }); + + it('falls back to Home when a prerequisite check stalls past the timeout', async () => { + selfAppState.selfApp.endpoint = '0xABC'; + getWhiteListedDisclosureAddresses.mockResolvedValue([ + { + contract_address: '0xabc', + points_per_disclosure: 25, + }, + ]); + // Simulate a hung network call — never resolves. + hasUserDoneThePointsDisclosure.mockImplementation( + () => new Promise(() => {}), + ); + + render(); + + await waitFor(() => { + expect(getWhiteListedDisclosureAddresses).toHaveBeenCalledTimes(1); + }); + + fireEvent.press(screen.getByTestId('primary-button')); + + // Advance past the 3s prereq timeout. + await act(async () => { + jest.advanceTimersByTime(3000); + }); + + await waitFor(() => { + expect(mockGoHome).toHaveBeenCalledTimes(1); + }); + expect(mockNavigate).not.toHaveBeenCalledWith( + 'Gratification', + expect.anything(), + ); + + act(() => { + jest.advanceTimersByTime(2000); + }); + + expect(mockCleanSelfApp).toHaveBeenCalledTimes(1); + }); + it('does not clear self app state if a newer session replaces the completed one', async () => { render(); From 0d5eb6c26d106a9cf32c8e6bb772d98fe9d2920e Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Sun, 19 Apr 2026 18:56:22 -0700 Subject: [PATCH 3/3] bump version (#1995) --- app/android/app/build.gradle | 2 +- app/app.json | 2 +- app/ios/OpenPassport/Info.plist | 2 +- app/ios/Self.xcodeproj/project.pbxproj | 4 ++-- app/package.json | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/android/app/build.gradle b/app/android/app/build.gradle index cefbe704b..924a684c8 100644 --- a/app/android/app/build.gradle +++ b/app/android/app/build.gradle @@ -133,7 +133,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode 149 - versionName "2.9.17" + versionName "2.9.18" manifestPlaceholders = [appAuthRedirectScheme: 'com.proofofpassportapp'] externalNativeBuild { cmake { diff --git a/app/app.json b/app/app.json index 27c195db3..cc9938042 100644 --- a/app/app.json +++ b/app/app.json @@ -4,7 +4,7 @@ "expo": { "name": "Self App", "slug": "self-app", - "version": "2.9.17", + "version": "2.9.18", "platforms": ["ios", "android"], "ios": { "bundleIdentifier": "com.proofofpassportapp" diff --git a/app/ios/OpenPassport/Info.plist b/app/ios/OpenPassport/Info.plist index 1621fccc2..7cdad8669 100644 --- a/app/ios/OpenPassport/Info.plist +++ b/app/ios/OpenPassport/Info.plist @@ -21,7 +21,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2.9.17 + 2.9.18 CFBundleSignature ???? CFBundleURLTypes diff --git a/app/ios/Self.xcodeproj/project.pbxproj b/app/ios/Self.xcodeproj/project.pbxproj index d88ab8f35..22ec9580d 100644 --- a/app/ios/Self.xcodeproj/project.pbxproj +++ b/app/ios/Self.xcodeproj/project.pbxproj @@ -593,7 +593,7 @@ "$(PROJECT_DIR)", "$(PROJECT_DIR)/MoproKit/Libs", ); - MARKETING_VERSION = 2.9.17; + MARKETING_VERSION = 2.9.18; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -735,7 +735,7 @@ "$(PROJECT_DIR)", "$(PROJECT_DIR)/MoproKit/Libs", ); - MARKETING_VERSION = 2.9.17; + MARKETING_VERSION = 2.9.18; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", diff --git a/app/package.json b/app/package.json index 30d341b90..6391d2e3e 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "@selfxyz/mobile-app", - "version": "2.9.17", + "version": "2.9.18", "private": true, "type": "module", "scripts": {