From 5c6419cc2efa6f278cf02248996cedd1aafb2253 Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Tue, 14 Apr 2026 15:28:40 -0700 Subject: [PATCH 01/15] chore: clean up from 2.9.17 (#1978) * minor pionts fixes * pr feedback * bump build numbers --- app/android/app/build.gradle | 2 +- app/ios/Self.xcodeproj/project.pbxproj | 4 ++-- app/src/components/navbar/Points.tsx | 20 +++++++++----------- app/src/components/navbar/PointsNavBar.tsx | 3 ++- app/src/navigation/app.tsx | 3 ++- app/src/screens/app/GratificationScreen.tsx | 2 +- app/version.json | 8 ++++---- 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/app/android/app/build.gradle b/app/android/app/build.gradle index e154940f9..1079f101d 100644 --- a/app/android/app/build.gradle +++ b/app/android/app/build.gradle @@ -138,7 +138,7 @@ android { applicationId "com.proofofpassportapp" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 147 + versionCode 148 versionName "2.9.17" manifestPlaceholders = [appAuthRedirectScheme: 'com.proofofpassportapp'] externalNativeBuild { diff --git a/app/ios/Self.xcodeproj/project.pbxproj b/app/ios/Self.xcodeproj/project.pbxproj index 503271669..d88ab8f35 100644 --- a/app/ios/Self.xcodeproj/project.pbxproj +++ b/app/ios/Self.xcodeproj/project.pbxproj @@ -477,7 +477,7 @@ CODE_SIGN_ENTITLEMENTS = OpenPassport/OpenPassportDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 217; + CURRENT_PROJECT_VERSION = 218; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_APP_SANDBOX = NO; ENABLE_BITCODE = NO; @@ -620,7 +620,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = OpenPassport/OpenPassport.entitlements; - CURRENT_PROJECT_VERSION = 217; + CURRENT_PROJECT_VERSION = 218; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_APP_SANDBOX = NO; ENABLE_RESOURCE_ACCESS_CAMERA = YES; diff --git a/app/src/components/navbar/Points.tsx b/app/src/components/navbar/Points.tsx index d4611a485..e1fb704f7 100644 --- a/app/src/components/navbar/Points.tsx +++ b/app/src/components/navbar/Points.tsx @@ -56,6 +56,7 @@ const Points: React.FC = () => { const incomingPoints = useIncomingPoints(); const { amount: points } = usePoints(); const loadEvents = usePointEventStore(state => state.loadEvents); + const events = usePointEventStore(state => state.events); const { hasCompletedBackupForPoints, setBackupForPointsCompleted } = useSettingStore(); const [isBackingUp, setIsBackingUp] = useState(false); @@ -76,18 +77,15 @@ const Points: React.FC = () => { navigation.navigate('PointsInfo'); }; - //TODO - uncomment after merging - https://github.com/selfxyz/self/pull/1363/ - // useEffect(() => { - // const backupEvent = usePointEventStore - // .getState() - // .events.find( - // event => event.type === 'backup' && event.status === 'completed', - // ); + useEffect(() => { + const backupEvent = events.find( + event => event.type === 'backup' && event.status === 'completed', + ); - // if (backupEvent && !hasCompletedBackupForPoints) { - // setBackupForPointsCompleted(); - // } - // }, [setBackupForPointsCompleted, hasCompletedBackupForPoints]); + if (backupEvent && !hasCompletedBackupForPoints) { + setBackupForPointsCompleted(); + } + }, [events, setBackupForPointsCompleted, hasCompletedBackupForPoints]); // Track if we should check for backup completion on next focus const shouldCheckBackupRef = React.useRef(false); diff --git a/app/src/components/navbar/PointsNavBar.tsx b/app/src/components/navbar/PointsNavBar.tsx index 1e2f3a621..ec3c518a3 100644 --- a/app/src/components/navbar/PointsNavBar.tsx +++ b/app/src/components/navbar/PointsNavBar.tsx @@ -8,6 +8,7 @@ import type { NativeStackHeaderProps } from '@react-navigation/native-stack'; import { Text, View } from '@selfxyz/mobile-sdk-alpha/components'; import { black, slate50 } from '@selfxyz/mobile-sdk-alpha/constants/colors'; +import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts'; import { NavBar } from '@/components/navbar/BaseNavBar'; import { buttonTap } from '@/integrations/haptics'; @@ -39,7 +40,7 @@ export const PointsNavBar = (props: NativeStackHeaderProps) => { color={black} fontSize={15} fontWeight="500" - fontFamily="DINOT-Medium" + fontFamily={dinot} textAlign="center" style={{ letterSpacing: 0.6, diff --git a/app/src/navigation/app.tsx b/app/src/navigation/app.tsx index 766bed085..95c438c8e 100644 --- a/app/src/navigation/app.tsx +++ b/app/src/navigation/app.tsx @@ -6,6 +6,7 @@ import React from 'react'; import type { NativeStackNavigationOptions } from '@react-navigation/native-stack'; import type { DocumentCategory } from '@selfxyz/common/utils/types'; +import { black } from '@selfxyz/mobile-sdk-alpha/constants/colors'; import { SystemBars } from '@/components/SystemBars'; import DeferredLinkingInfoScreen from '@/screens/app/DeferredLinkingInfoScreen'; @@ -54,7 +55,7 @@ const appScreens = { screen: GratificationScreen, options: { headerShown: false, - contentStyle: { backgroundColor: '#000000' }, + contentStyle: { backgroundColor: black }, } as NativeStackNavigationOptions, params: {} as { points?: number; diff --git a/app/src/screens/app/GratificationScreen.tsx b/app/src/screens/app/GratificationScreen.tsx index 50aa8c858..b780a1d96 100644 --- a/app/src/screens/app/GratificationScreen.tsx +++ b/app/src/screens/app/GratificationScreen.tsx @@ -50,7 +50,7 @@ const GratificationScreen: React.FC = () => { }; const handleBackPress = () => { - navigation.navigate('Points'); + navigation.goBack(); }; const handleAnimationFinish = useCallback(() => { diff --git a/app/version.json b/app/version.json index a9abe420e..7335f7191 100644 --- a/app/version.json +++ b/app/version.json @@ -1,10 +1,10 @@ { "ios": { - "build": 217, - "lastDeployed": "2026-04-14T04:50:15.630Z" + "build": 218, + "lastDeployed": "2026-04-14T21:02:24.000Z" }, "android": { - "build": 147, - "lastDeployed": "2026-04-14T02:03:03.238Z" + "build": 148, + "lastDeployed": "2026-04-14T21:02:24.000Z" } } From 20c3fa5140588ade607c928fb923e065a83036ca Mon Sep 17 00:00:00 2001 From: Leszek Stachowski Date: Wed, 15 Apr 2026 00:29:53 +0200 Subject: [PATCH 02/15] chore: create Self dictionary (#1974) --- CLAUDE.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index fbf228a7f..4d14b4a05 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -45,6 +45,10 @@ nvm use && corepack enable && yarn install - **Constraint tie-breaker.** If rules conflict: correctness and security first, then scope/clarity (small PRs, small files), then reuse. Document the tradeoff in the spec. - **Linear issue descriptions are immutable after creation.** Never overwrite an issue description with `save_issue` to add updates, status notes, or context. Issue descriptions are the original scope set at creation time. All subsequent updates — status changes, progress notes, discovered context, blockers, decision records — go in **comments** via `save_comment`. The only valid use of `save_issue` on an existing issue is to change structured fields (status, priority, assignee, labels). If you need to correct a factual error in the description, add a comment explaining the correction rather than silently rewriting history. +## Project Dictionary + +When asked about an unfamiliar project term, **look it up in the [Self Dictionary](https://www.notion.so/34257801cd1280a4b348d01fac82a2be) in Notion first** — before searching the codebase. The dictionary is the authoritative source for project terminology. + ## Specs & Planning **Every feature — even minor ones — needs a spec.** For SDK work (`packages/`, `webview-app`, `webview-bridge`), specs live in **both** the repo (`specs/`) and Linear. The repo spec is the canonical, version-controlled execution plan. The Linear issue is the tracking and discovery layer. For app-only or non-SDK work, a Linear issue with inline scope is sufficient — no repo spec required. From 63d6a96356456c38b4a7bb0f85db373f99f23c3e Mon Sep 17 00:00:00 2001 From: "Seshanth.S" <35675963+seshanthS@users.noreply.github.com> Date: Wed, 15 Apr 2026 08:08:06 +0530 Subject: [PATCH 03/15] Add tap-through demo mode for tunnel verification flow (#1977) * Fix navigation in disclose result related screens * coderabbit comments * add demo flow * Enable conditional rendering for KYC pending screen and update mock provider result handling in development mode * Update packages/webview-app/src/utils/mockOnboardingFlow.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * fix hook --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: Justin Hernandez --- packages/webview-app/src/App.tsx | 2 + .../onboarding/ProviderLaunchScreen.tsx | 10 ++-- .../onboarding/ProviderResultScreen.tsx | 12 +++- .../screens/tunnel/TunnelDiscloseScreen.tsx | 60 ++++++++++++++++++- .../screens/tunnel/TunnelKycPendingScreen.tsx | 39 ++++++++++++ .../screens/tunnel/TunnelKycSuccessScreen.tsx | 11 ++-- .../src/screens/tunnel/TunnelKycWrapper.tsx | 17 +++++- .../screens/tunnel/TunnelProvingScreen.tsx | 46 +++++++++++++- .../src/screens/tunnel/TunnelResultScreen.tsx | 18 +++++- .../src/utils/mockOnboardingFlow.ts | 17 +++++- 10 files changed, 213 insertions(+), 19 deletions(-) create mode 100644 packages/webview-app/src/screens/tunnel/TunnelKycPendingScreen.tsx diff --git a/packages/webview-app/src/App.tsx b/packages/webview-app/src/App.tsx index 07a55c64b..7cf671335 100644 --- a/packages/webview-app/src/App.tsx +++ b/packages/webview-app/src/App.tsx @@ -55,6 +55,7 @@ import { TunnelCountryPickerScreen } from './screens/tunnel/TunnelCountryPickerS import { TunnelDiscloseScreen } from './screens/tunnel/TunnelDiscloseScreen'; import { TunnelIDTypeScreen } from './screens/tunnel/TunnelIDTypeScreen'; import { TunnelKycFailureScreen } from './screens/tunnel/TunnelKycFailureScreen'; +import { TunnelKycPendingScreen } from './screens/tunnel/TunnelKycPendingScreen'; import { TunnelKycSuccessScreen } from './screens/tunnel/TunnelKycSuccessScreen'; import { TunnelKycWrapper } from './screens/tunnel/TunnelKycWrapper'; import { TunnelProofReceiptScreen } from './screens/tunnel/TunnelProofReceiptScreen'; @@ -112,6 +113,7 @@ export const App: React.FC = () => ( } /> } /> } /> + {import.meta.env.DEV && } />} } /> } /> } /> diff --git a/packages/webview-app/src/screens/onboarding/ProviderLaunchScreen.tsx b/packages/webview-app/src/screens/onboarding/ProviderLaunchScreen.tsx index d7ce7cdef..9d43b64a6 100644 --- a/packages/webview-app/src/screens/onboarding/ProviderLaunchScreen.tsx +++ b/packages/webview-app/src/screens/onboarding/ProviderLaunchScreen.tsx @@ -127,7 +127,7 @@ export const ProviderLaunchScreen: React.FC = () => { let cancelled = false; const controller = new AbortController(); - if (environment !== 'prod') { + if (environment !== 'prod' && !defaultNextPath.includes('mock=demo')) { (async () => { try { setPhase('waiting'); @@ -306,12 +306,12 @@ export const ProviderLaunchScreen: React.FC = () => { {phase === 'waiting' && (
{ const state = (location.state as ({ providerResult?: KycProviderResult } & MockOnboardingNavigationState) | null) ?? null; - const providerResult = state?.providerResult ?? createMockProviderResult({ outcome: mockOutcome }); + const providerResult = + state?.providerResult ?? (import.meta.env.DEV ? createMockProviderResult({ outcome: mockOutcome }) : undefined); useEffect(() => { + if (!providerResult) { + navigate('/', { replace: true }); + return; + } + haptic.trigger('selection'); analytics.trackEvent('provider_result_received', { status: providerResult.status, @@ -79,8 +85,8 @@ export const ProviderResultScreen: React.FC = () => { lifecycle, mockOutcome, navigate, - providerResult.error?.retryable, - providerResult.status, + providerResult?.error?.retryable, + providerResult?.status, state?.countryCode, state?.documentType, ]); diff --git a/packages/webview-app/src/screens/tunnel/TunnelDiscloseScreen.tsx b/packages/webview-app/src/screens/tunnel/TunnelDiscloseScreen.tsx index 3cb411100..9608cc1e3 100644 --- a/packages/webview-app/src/screens/tunnel/TunnelDiscloseScreen.tsx +++ b/packages/webview-app/src/screens/tunnel/TunnelDiscloseScreen.tsx @@ -4,7 +4,7 @@ import type React from 'react'; import { useCallback, useEffect, useRef, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import type { ProofGenerationStep } from '@selfxyz/euclid'; import { ProofProgressScreen, SelfLogo } from '@selfxyz/euclid'; @@ -14,8 +14,54 @@ import { useProvingStore } from '@selfxyz/mobile-sdk-alpha/browser'; import { useSelfClient } from '../../providers/SelfClientProvider'; import { useVerificationRequest } from '../../providers/VerificationRequestProvider'; import { WEB_SAFE_AREA } from '../../utils/insets'; +import { isDemoMode } from '../../utils/mockOnboardingFlow'; import { initSelfAppFromRequest } from '../../utils/selfAppContext'; +const DEMO_DISCLOSE_STEPS: ProofGenerationStep[] = [ + 'readingRegistry', + 'generatingProof', + 'awaitingVerification', + 'finishingUp', +]; + +const DemoTunnelDiscloseScreen: React.FC<{ search: string }> = ({ search }) => { + const navigate = useNavigate(); + const { appName, appEndpoint, timestamp } = useVerificationRequest(); + const [stepIndex, setStepIndex] = useState(0); + + const advance = useCallback(() => { + if (stepIndex < DEMO_DISCLOSE_STEPS.length - 1) { + setStepIndex(i => i + 1); + } else { + navigate(`/tunnel/proof/result${search}`, { replace: true, state: { success: true } }); + } + }, [stepIndex, navigate, search]); + + return ( +
+ } + appName={appName} + appEndpoint={appEndpoint} + documentType="passport" + timestamp={timestamp} + step={DEMO_DISCLOSE_STEPS[stepIndex]} + /> +
+
+ ); +}; + const MAX_DISCLOSE_RETRIES = 3; const DISCLOSE_RETRY_DELAY_MS = 3000; const ERROR_STATES: ProvingStateType[] = ['error', 'failure', 'passport_not_supported', 'passport_data_not_found']; @@ -42,7 +88,7 @@ function mapDiscloseStateToStep(state: ProvingStateType | null): ProofGeneration } } -export const TunnelDiscloseScreen: React.FC = () => { +const StandardTunnelDiscloseScreen: React.FC = () => { const navigate = useNavigate(); const { client, analytics, haptic } = useSelfClient(); const verificationCtx = useVerificationRequest(); @@ -151,3 +197,13 @@ export const TunnelDiscloseScreen: React.FC = () => { /> ); }; + +export const TunnelDiscloseScreen: React.FC = () => { + const location = useLocation(); + + if (isDemoMode(location.search)) { + return ; + } + + return ; +}; diff --git a/packages/webview-app/src/screens/tunnel/TunnelKycPendingScreen.tsx b/packages/webview-app/src/screens/tunnel/TunnelKycPendingScreen.tsx new file mode 100644 index 000000000..befe9ed09 --- /dev/null +++ b/packages/webview-app/src/screens/tunnel/TunnelKycPendingScreen.tsx @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import type React from 'react'; +import { useCallback } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; + +import { KycPendingScreen } from '@selfxyz/euclid'; + +import { WEB_SAFE_AREA } from '../../utils/insets'; +import { createMockProviderResult } from '../../utils/mockOnboardingFlow'; + +export const TunnelKycPendingScreen: React.FC = () => { + const navigate = useNavigate(); + const location = useLocation(); + + const handleTap = useCallback(() => { + navigate(`/tunnel/kyc-success${location.search}`, { + state: { providerResult: createMockProviderResult({ outcome: 'demo' }) }, + }); + }, [navigate, location.search]); + + return ( +
+ {}} onReceiveLiveUpdates={() => {}} /> +
+
+ ); +}; diff --git a/packages/webview-app/src/screens/tunnel/TunnelKycSuccessScreen.tsx b/packages/webview-app/src/screens/tunnel/TunnelKycSuccessScreen.tsx index 9ce66f91b..4a6fa6ccf 100644 --- a/packages/webview-app/src/screens/tunnel/TunnelKycSuccessScreen.tsx +++ b/packages/webview-app/src/screens/tunnel/TunnelKycSuccessScreen.tsx @@ -11,6 +11,7 @@ import { KycVerificationSuccessScreen } from '@selfxyz/euclid'; import { useSelfClient } from '../../providers/SelfClientProvider'; import type { KycProviderResult } from '../../types/kycProvider'; import { WEB_SAFE_AREA } from '../../utils/insets'; +import { isDemoMode } from '../../utils/mockOnboardingFlow'; export const TunnelKycSuccessScreen: React.FC = () => { const navigate = useNavigate(); @@ -20,8 +21,10 @@ export const TunnelKycSuccessScreen: React.FC = () => { const state = location.state as { providerResult?: KycProviderResult } | null; const providerResult = state?.providerResult; + const demo = isDemoMode(location.search); + useEffect(() => { - if (!providerResult) return; + if (demo || !providerResult) return; if (providerResult.status === 'cancel') { navigate('/tunnel/tour/4', { replace: true }); @@ -36,13 +39,13 @@ export const TunnelKycSuccessScreen: React.FC = () => { } return; } - }, [providerResult, navigate]); + }, [demo, providerResult, navigate]); const onGenerateProof = useCallback(() => { haptic.trigger('success'); analytics.trackEvent('tunnel_kyc_success_generate_proof'); - navigate('/tunnel/proof/generating'); - }, [navigate, haptic, analytics]); + navigate(`/tunnel/proof/generating${location.search}`); + }, [navigate, location.search, haptic, analytics]); return ; }; diff --git a/packages/webview-app/src/screens/tunnel/TunnelKycWrapper.tsx b/packages/webview-app/src/screens/tunnel/TunnelKycWrapper.tsx index e8bc57cf4..e60bf7413 100644 --- a/packages/webview-app/src/screens/tunnel/TunnelKycWrapper.tsx +++ b/packages/webview-app/src/screens/tunnel/TunnelKycWrapper.tsx @@ -5,7 +5,7 @@ import type React from 'react'; import { Navigate, useLocation } from 'react-router-dom'; -import { createMockProviderResult, getMockOutcomeFromSearch } from '../../utils/mockOnboardingFlow'; +import { createMockProviderResult, getMockOutcomeFromSearch, isDemoMode } from '../../utils/mockOnboardingFlow'; /** * Redirects `/tunnel/kyc` to `ProviderLaunchScreen` at `/onboarding/provider` @@ -19,6 +19,21 @@ export const TunnelKycWrapper: React.FC = () => { const incomingState = (location.state as Record) ?? {}; const mockOutcome = getMockOutcomeFromSearch(location.search); + if (isDemoMode(location.search)) { + const pendingPath = `/tunnel/kyc-pending${location.search}`; + return ( + + ); + } + if (import.meta.env.DEV && location.search.includes('mock=')) { return ( = ({ search }) => { + const navigate = useNavigate(); + const [stepIndex, setStepIndex] = useState(0); + + const advance = useCallback(() => { + if (stepIndex < DEMO_PROVING_STEPS.length - 1) { + setStepIndex(i => i + 1); + } else { + navigate(`/tunnel/proof/disclose${search}`, { replace: true }); + } + }, [stepIndex, navigate, search]); + + return ( +
+ +
+
+ ); +}; + type Phase = 'dsc' | 'register'; const ERROR_STATES: ProvingStateType[] = ['error', 'failure', 'passport_not_supported', 'passport_data_not_found']; @@ -42,7 +74,7 @@ function mapProvingStateToStep(state: ProvingStateType | null, phase: Phase): Pr } } -export const TunnelProvingScreen: React.FC = () => { +const StandardTunnelProvingScreen: React.FC = () => { const navigate = useNavigate(); const { client, analytics, haptic } = useSelfClient(); const verificationCtx = useVerificationRequest(); @@ -120,3 +152,13 @@ export const TunnelProvingScreen: React.FC = () => { /> ); }; + +export const TunnelProvingScreen: React.FC = () => { + const location = useLocation(); + + if (isDemoMode(location.search)) { + return ; + } + + return ; +}; diff --git a/packages/webview-app/src/screens/tunnel/TunnelResultScreen.tsx b/packages/webview-app/src/screens/tunnel/TunnelResultScreen.tsx index 3db6a4e73..f3c13722f 100644 --- a/packages/webview-app/src/screens/tunnel/TunnelResultScreen.tsx +++ b/packages/webview-app/src/screens/tunnel/TunnelResultScreen.tsx @@ -12,6 +12,7 @@ import type { VerificationResult } from '@selfxyz/webview-bridge'; import { useSelfClient } from '../../providers/SelfClientProvider'; import { useVerificationRequest } from '../../providers/VerificationRequestProvider'; import { WEB_SAFE_AREA } from '../../utils/insets'; +import { isDemoMode } from '../../utils/mockOnboardingFlow'; interface TunnelResultState { success?: boolean; @@ -44,7 +45,22 @@ export const TunnelResultScreen: React.FC = () => { analytics.trackEvent('tunnel_result_failure', { error }); }, [success, error, analytics]); + const demo = isDemoMode(location.search); + const onContinue = useCallback(async () => { + if (demo) { + const demoResult: VerificationResult = { + success: true, + userId: request.userId, + verificationId, + claims: { resultType: 'proofRequested' }, + }; + await lifecycle.setResult(demoResult); + analytics.trackEvent('tunnel_result_success'); + lifecycle.dismiss(); + return; + } + try { const result: VerificationResult = { success: true, @@ -60,7 +76,7 @@ export const TunnelResultScreen: React.FC = () => { error: err instanceof Error ? err.message : 'Failed to send result', }); } - }, [request.userId, verificationId, lifecycle, analytics]); + }, [demo, request.userId, verificationId, lifecycle, analytics]); const onRetry = useCallback(() => { navigate(getTunnelBackPath(source), { replace: true }); diff --git a/packages/webview-app/src/utils/mockOnboardingFlow.ts b/packages/webview-app/src/utils/mockOnboardingFlow.ts index b1a6a6e6d..6aad8dd43 100644 --- a/packages/webview-app/src/utils/mockOnboardingFlow.ts +++ b/packages/webview-app/src/utils/mockOnboardingFlow.ts @@ -11,7 +11,7 @@ export interface MockOnboardingNavigationState { nextPath?: string; } -export type MockRegistrationOutcome = 'success' | 'kyc-failure' | 'registration-failure' | 'cancel'; +export type MockRegistrationOutcome = 'success' | 'kyc-failure' | 'registration-failure' | 'cancel' | 'demo'; export type PromptMockState = 'default' | 'existing-account'; const DEFAULT_OUTCOME: MockRegistrationOutcome = 'success'; @@ -25,6 +25,10 @@ export const createMockProviderResult = ({ outcome: MockRegistrationOutcome; verificationId?: string; }): KycProviderResult => { + if (!MOCKS_ENABLED) { + throw new Error('createMockProviderResult must not be called outside dev mode'); + } + const resolvedVerificationId = verificationId ?? 'mock-verification'; switch (outcome) { @@ -71,6 +75,13 @@ export const createMockProviderResult = ({ retryable: true, }, }; + case 'demo': + return { + status: 'success', + verificationId: resolvedVerificationId, + provider: 'mock-provider', + completedAt: new Date().toISOString(), + }; } }; @@ -86,6 +97,7 @@ export const getMockOutcomeFromSearch = (search: string): MockRegistrationOutcom case 'kyc-failure': case 'registration-failure': case 'cancel': + case 'demo': return value; default: return DEFAULT_OUTCOME; @@ -116,3 +128,6 @@ export const getPromptMockSearch = (mock: PromptMockState = DEFAULT_PROMPT_MOCK) export const getProviderPath = (outcome: MockRegistrationOutcome): string => `/onboarding/provider${getMockOutcomeSearch(outcome)}`; + +export const isDemoMode = (search: string): boolean => + MOCKS_ENABLED && new URLSearchParams(search).get('mock') === 'demo'; From a825d8fc32c108aeb3b6f22d40d2506218239126 Mon Sep 17 00:00:00 2001 From: Nesopie <87437291+Nesopie@users.noreply.github.com> Date: Thu, 16 Apr 2026 11:17:11 +0530 Subject: [PATCH 04/15] fix: add revealed data view for KYC ID cards (#1980) The "View ID Data" button was broken for didit KYC documents because KycIdCard ignored the hidden prop. This adds a revealed view showing name, DOB, nationality, document number, gender, and dates. Co-authored-by: Claude Opus 4.6 (1M context) --- app/src/components/homescreen/KycIdCard.tsx | 203 ++++++++++++++++++-- 1 file changed, 191 insertions(+), 12 deletions(-) diff --git a/app/src/components/homescreen/KycIdCard.tsx b/app/src/components/homescreen/KycIdCard.tsx index e27105091..51e63eb07 100644 --- a/app/src/components/homescreen/KycIdCard.tsx +++ b/app/src/components/homescreen/KycIdCard.tsx @@ -4,22 +4,34 @@ import type { FC } from 'react'; import React from 'react'; -import { Image } from 'react-native'; -import { YStack } from 'tamagui'; +import { Dimensions, Image } from 'react-native'; +import { Separator, Text, XStack, YStack } from 'tamagui'; import { deserializeApplicantInfo } from '@selfxyz/common'; import { commonNames } from '@selfxyz/common/constants/countries'; import type { KycData } from '@selfxyz/common/utils/types'; import { RoundFlag } from '@selfxyz/mobile-sdk-alpha/components'; -import { black, white } from '@selfxyz/mobile-sdk-alpha/constants/colors'; +import { + black, + separatorColor, + slate100, + slate400, + white, +} from '@selfxyz/mobile-sdk-alpha/constants/colors'; +import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts'; import CardBackgroundId1 from '@/assets/images/card_background_id1.png'; +import LogoGray from '@/assets/images/logo_gray.svg'; import SelfLogoPending from '@/assets/images/self_logo_pending.svg'; import CardBottomContent from '@/components/homescreen/CardBottomContent'; import CardHeader from '@/components/homescreen/CardHeader'; import { cardStyles } from '@/components/homescreen/cardStyles'; +import IdAttribute from '@/components/homescreen/IdAttribute'; import { useCardDimensions } from '@/hooks/useCardDimensions'; -import { getCountryAdjective } from '@/utils/countryDemonyms'; +import { + getCountryAdjective, + getCountryDemonym, +} from '@/utils/countryDemonyms'; interface KycIdCardProps { idDocument: KycData; @@ -44,18 +56,23 @@ function getKycDocTitle(idType: string): string { } /** - * KYC document card - matches IdCard design exactly but shows "STANDARD" badge. - * Used for documents verified through KYC flow (drivers license, etc.). + * Format YYYYMMDD to DD/MM/YYYY for display. */ -const KycIdCard: FC = ({ - idDocument, - selected, - hidden: _hidden, -}) => { +function formatKycDate(date: string): string { + if (date.length !== 8) return date; + return `${date.slice(6, 8)}/${date.slice(4, 6)}/${date.slice(0, 4)}`; +} + +const KycIdCard: FC = ({ idDocument, selected, hidden }) => { // Extract KYC fields from serialized applicant info with error handling let country = ''; let idType = ''; let idNumber = ''; + let fullName = ''; + let dob = ''; + let gender = ''; + let expiryDate = ''; + let issuanceDate = ''; try { const applicantInfo = deserializeApplicantInfo( @@ -64,12 +81,16 @@ const KycIdCard: FC = ({ country = applicantInfo.country || ''; idType = applicantInfo.idType || ''; idNumber = applicantInfo.idNumber || ''; + fullName = applicantInfo.fullName || ''; + dob = applicantInfo.dob || ''; + gender = applicantInfo.gender || ''; + expiryDate = applicantInfo.expiryDate || ''; + issuanceDate = applicantInfo.issuanceDate || ''; } catch (error) { console.error( '[KycIdCard] Failed to deserialize applicant info, using fallback values:', error, ); - // Fallback to safe defaults - component will render generic "ID CARD" display } const docTitle = getKycDocTitle(idType); @@ -106,6 +127,164 @@ const KycIdCard: FC = ({ // Bottom label (e.g., "US DRIVERS LICENSE") const bottomLabel = `${countryAdj} ${docTitle}`; + // Revealed data view + if (!hidden && selected) { + const { width: screenWidth } = Dimensions.get('window'); + const revealedWidth = screenWidth * 0.95 - 16; + const revealedHeight = revealedWidth * 0.645; + const revealedBorderRadius = revealedWidth * 0.04; + const revealedPadding = revealedWidth * 0.035; + const revealedFontSize = { + large: revealedWidth * 0.045, + small: revealedWidth * 0.028, + }; + const imageSize = { + width: revealedWidth * 0.2, + height: revealedWidth * 0.29, + }; + const contentLeftOffset = imageSize.width + revealedPadding; + const countryName = getCountryDemonym(country); + + return ( + + + {/* Header */} + + + + + {docTitle} + + + Verified {countryName} {docTitle} + + + + + + + {/* Attributes Grid */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* Footer */} + + + + + + + + ); + } + return ( Date: Fri, 17 Apr 2026 12:10:19 +0530 Subject: [PATCH 05/15] SELF-2643: use expo-camera for qrcode scanning (#1981) * use expo-camera for qrcode scanning * remove unused react import * greptile review comments * coderabbit comments * lint * update podfile.lock --------- Co-authored-by: Justin Hernandez --- app/android/build.gradle | 1 - app/ios/Podfile.lock | 15 +++ app/jest.config.cjs | 1 + app/package.json | 1 + app/src/components/native/QRCodeScanner.tsx | 141 ++++++++------------ app/tests/__setup__/expoCameraMock.js | 8 ++ yarn.lock | 18 +++ 7 files changed, 98 insertions(+), 87 deletions(-) create mode 100644 app/tests/__setup__/expoCameraMock.js diff --git a/app/android/build.gradle b/app/android/build.gradle index 6d7eafc12..a5e284ed1 100644 --- a/app/android/build.gradle +++ b/app/android/build.gradle @@ -49,7 +49,6 @@ allprojects { substitute(platform(module('com.gemalto.jp2:jp2-android'))) using module('com.github.Tgo1014:JP2ForAndroid:1.0.4') substitute module('io.fotoapparat:fotoapparat') using module('com.github.fotoapparat:fotoapparat:2.7.0') } - resolutionStrategy.force 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava' } configurations.all { resolutionStrategy { diff --git a/app/ios/Podfile.lock b/app/ios/Podfile.lock index 2a16a6ec6..994f7415e 100644 --- a/app/ios/Podfile.lock +++ b/app/ios/Podfile.lock @@ -27,6 +27,10 @@ PODS: - React-Core - ExpoAsset (11.0.5): - ExpoModulesCore + - ExpoCamera (16.0.18): + - ExpoModulesCore + - ZXingObjC/OneD + - ZXingObjC/PDF417 - ExpoFileSystem (18.0.12): - ExpoModulesCore - ExpoFont (13.0.4): @@ -2218,6 +2222,11 @@ PODS: - SwiftyTesseract (3.1.3) - Yoga (0.0.0) - YttriumWrapper (0.10.50) + - ZXingObjC/Core (3.6.9) + - ZXingObjC/OneD (3.6.9): + - ZXingObjC/Core + - ZXingObjC/PDF417 (3.6.9): + - ZXingObjC/Core DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) @@ -2229,6 +2238,7 @@ DEPENDENCIES: - Expo (from `../node_modules/expo`) - "ExpoAdapterGoogleSignIn (from `../node_modules/@react-native-google-signin/google-signin/expo/ios`)" - ExpoAsset (from `../node_modules/expo-asset/ios`) + - ExpoCamera (from `../node_modules/expo-camera/ios`) - ExpoFileSystem (from `../node_modules/expo-file-system/ios`) - ExpoFont (from `../node_modules/expo-font/ios`) - ExpoKeepAwake (from `../node_modules/expo-keep-awake/ios`) @@ -2371,6 +2381,7 @@ SPEC REPOS: - SocketRocket - SwiftyTesseract - YttriumWrapper + - ZXingObjC EXTERNAL SOURCES: boost: @@ -2391,6 +2402,8 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-google-signin/google-signin/expo/ios" ExpoAsset: :path: "../node_modules/expo-asset/ios" + ExpoCamera: + :path: "../node_modules/expo-camera/ios" ExpoFileSystem: :path: "../node_modules/expo-file-system/ios" ExpoFont: @@ -2620,6 +2633,7 @@ SPEC CHECKSUMS: Expo: 4bb70893882e6382b41d1e910d7226c6a1b85f0a ExpoAdapterGoogleSignIn: ab4d9fc38cb91077a4138d178395525ec65d0c2e ExpoAsset: 48386d40d53a8c1738929b3ed509bcad595b5516 + ExpoCamera: 0a3e78de7b1ca8c438ee6784fa6f22c5b1b36966 ExpoFileSystem: 42d363d3b96f9afab980dcef60d5657a4443c655 ExpoFont: f354e926f8feae5e831ec8087f36652b44a0b188 ExpoKeepAwake: b0171a73665bfcefcfcc311742a72a956e6aa680 @@ -2751,6 +2765,7 @@ SPEC CHECKSUMS: SwiftyTesseract: 1f3d96668ae92dc2208d9842c8a59bea9fad2cbb Yoga: c34725819ab0a5962e85455b9e56679b306910ee YttriumWrapper: d7f63336830536f1da41b745ed8bacedb04228c4 + ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5 PODFILE CHECKSUM: 83f631d1b6308502a035e656b2f9dfab30431ae3 diff --git a/app/jest.config.cjs b/app/jest.config.cjs index 637f58d9e..3ebd75154 100644 --- a/app/jest.config.cjs +++ b/app/jest.config.cjs @@ -36,6 +36,7 @@ module.exports = { '^@$': '/src', '^@tests/(.*)$': '/tests/src/$1', '^@tests$': '/tests/src', + '^expo-camera$': '/tests/__setup__/expoCameraMock.js', // Map react-native-svg to app's node_modules for all packages '^react-native-svg$': '/node_modules/react-native-svg', '^@selfxyz/mobile-sdk-alpha$': diff --git a/app/package.json b/app/package.json index 3cbb117af..30d341b90 100644 --- a/app/package.json +++ b/app/package.json @@ -127,6 +127,7 @@ "ethers": "^6.16.0", "expo": "~52.0.40", "expo-application": "~6.0.2", + "expo-camera": "~16.0.18", "hash.js": "^1.1.7", "js-sha1": "^0.7.0", "js-sha256": "^0.11.1", diff --git a/app/src/components/native/QRCodeScanner.tsx b/app/src/components/native/QRCodeScanner.tsx index 0b7a885a2..14c25fa48 100644 --- a/app/src/components/native/QRCodeScanner.tsx +++ b/app/src/components/native/QRCodeScanner.tsx @@ -2,36 +2,13 @@ // SPDX-License-Identifier: BUSL-1.1 // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. -import React, { useCallback } from 'react'; -import type { NativeSyntheticEvent, StyleProp, ViewStyle } from 'react-native'; -import { PixelRatio, Platform, requireNativeComponent } from 'react-native'; - -import { RCTFragment } from '@/components/native/RCTFragment'; - -interface NativeQRCodeScannerViewProps { - onQRData: (event: NativeSyntheticEvent<{ data: string }>) => void; - onError: ( - event: NativeSyntheticEvent<{ - error: string; - errorMessage: string; - stackTrace: string; - }>, - ) => void; - style?: StyleProp; -} - -const QRCodeNativeComponent = Platform.select({ - ios: requireNativeComponent( - 'QRCodeScannerView', - ), - android: requireNativeComponent( - 'QRCodeScannerViewManager', - ), -}); - -if (!QRCodeNativeComponent) { - throw new Error('QRCodeScannerView not registered for this platform'); -} +import { + type BarcodeScanningResult, + CameraView, + useCameraPermissions, +} from 'expo-camera'; +import React, { useCallback, useEffect, useRef } from 'react'; +import { StyleSheet, View } from 'react-native'; export interface QRCodeScannerViewProps { isMounted: boolean; @@ -42,70 +19,62 @@ export const QRCodeScannerView: React.FC = ({ onQRData, isMounted, }) => { - const _onError = useCallback( - ( - event: NativeSyntheticEvent<{ - error: string; - errorMessage: string; - stackTrace: string; - }>, - ) => { - if (!isMounted) { + const [permission, requestPermission] = useCameraPermissions(); + + useEffect(() => { + if (permission && !permission.granted) { + if (permission.canAskAgain) { + requestPermission(); + } else { + onQRData(new Error('Camera permission denied')); + } + } + }, [permission, requestPermission, onQRData]); + + const hasScanned = useRef(false); + + const handleBarcodeScanned = useCallback( + (result: BarcodeScanningResult) => { + if (!isMounted || hasScanned.current) { return; } - const { - error: nativeError, - errorMessage, - stackTrace, - } = event.nativeEvent; - const e = new Error(errorMessage); - e.name = nativeError; - e.stack = stackTrace; - onQRData(e); + hasScanned.current = true; + onQRData(null, result.data); }, [onQRData, isMounted], ); - const _onQRData = useCallback( - (event: NativeSyntheticEvent<{ data: string }>) => { - if (!isMounted) { - return; - } - onQRData(null, event.nativeEvent.data); + const handleMountError = useCallback( + (event: { message: string }) => { + if (!isMounted) return; + onQRData(new Error(event.message)); }, - [onQRData, isMounted], + [isMounted, onQRData], ); - if (Platform.OS === 'ios') { - return ( - - ); - } else { - // For Android, wrap the native component inside your RCTFragment to preserve existing functionality. - const Fragment = RCTFragment as React.FC< - React.ComponentProps & NativeQRCodeScannerViewProps - >; - return ( - - } - fragmentComponentName="QRCodeScannerViewManager" - isMounted={isMounted} - style={{ - height: PixelRatio.getPixelSizeForLayoutSize(800), - width: PixelRatio.getPixelSizeForLayoutSize(400), - }} - onError={_onError} - onQRData={_onQRData} - /> - ); + if (!permission?.granted) { + return null; } + + return ( + + + + ); }; + +const styles = StyleSheet.create({ + container: { + width: '110%', + height: '110%', + }, + camera: { + flex: 1, + }, +}); diff --git a/app/tests/__setup__/expoCameraMock.js b/app/tests/__setup__/expoCameraMock.js new file mode 100644 index 000000000..a9e7431d7 --- /dev/null +++ b/app/tests/__setup__/expoCameraMock.js @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +module.exports = { + CameraView: 'CameraView', + useCameraPermissions: () => [{ granted: true, canAskAgain: true }, jest.fn()], +}; diff --git a/yarn.lock b/yarn.lock index d513cc3e9..4ac22e534 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11255,6 +11255,7 @@ __metadata: ethers: "npm:^6.16.0" expo: "npm:~52.0.40" expo-application: "npm:~6.0.2" + expo-camera: "npm:~16.0.18" hash.js: "npm:^1.1.7" hermes-eslint: "npm:^0.33.3" jest: "npm:^30.3.0" @@ -24698,6 +24699,23 @@ __metadata: languageName: node linkType: hard +"expo-camera@npm:~16.0.18": + version: 16.0.18 + resolution: "expo-camera@npm:16.0.18" + dependencies: + invariant: "npm:^2.2.4" + peerDependencies: + expo: "*" + react: "*" + react-native: "*" + react-native-web: "*" + peerDependenciesMeta: + react-native-web: + optional: true + checksum: 10c0/19d6f27af22d6e148b0bd7dc1dc37778b155ce0454515045a07e7dcc2b0474aa3d0d0986684fcbfe16837cffa41b33128d23376454d975b09b592c3dd92fe803 + languageName: node + linkType: hard + "expo-constants@npm:~17.0.8": version: 17.0.8 resolution: "expo-constants@npm:17.0.8" From 1042d27f54bd90d44a8d0a715f0eecea24aa715a Mon Sep 17 00:00:00 2001 From: turnoffthiscomputer <98749896+remicolin@users.noreply.github.com> Date: Fri, 17 Apr 2026 15:35:30 +0200 Subject: [PATCH 06/15] =?UTF-8?q?fix:=20patch=20Podfile=20for=20WalletConn?= =?UTF-8?q?ect=20Pay=20removal=20and=20Haptic=20feedback=20=E2=80=A6=20(#1?= =?UTF-8?q?982)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: patch Podfile for WalletConnect Pay removal and Haptic feedback framework addition Removed the YttriumWrapper dependency from the react-native-compat podspec as it is not used in the Self app, preventing build failures. Additionally, patched the react-native-haptic-feedback podspec to include the AudioToolbox framework, ensuring proper functionality of haptic feedback features. * Update app/ios/Podfile Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Update app/ios/Podfile Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- app/ios/Podfile | 36 ++++++++++ app/ios/Podfile.lock | 167 +++++++++++++++++++++---------------------- 2 files changed, 119 insertions(+), 84 deletions(-) diff --git a/app/ios/Podfile b/app/ios/Podfile index fe16e519a..a1b0d67ee 100755 --- a/app/ios/Podfile +++ b/app/ios/Podfile @@ -83,6 +83,42 @@ def using_https_git_auth? end end +# ── WalletConnect Pay / YttriumWrapper removal ────────────────────────────── +# react-native-compat v2.23+ bundles WalletConnect Pay which depends on +# YttriumWrapper (Yttrium UniFFI bindings). The Self app does not use +# WalletConnect Pay and the binary pod fails to build. Patch the podspec +# and remove the Pay bridge Swift file before CocoaPods resolves deps. +wc_compat_dir = File.join(__dir__, "..", "node_modules", "@walletconnect", "react-native-compat") +wc_podspec = File.join(wc_compat_dir, "react-native-compat.podspec") + +if File.exist?(wc_podspec) + podspec_text = File.read(wc_podspec) + if podspec_text.include?('YttriumWrapper') + podspec_text.gsub!(/^\s*s\.dependency\s+"YttriumWrapper".*$/, ' # s.dependency "YttriumWrapper" — removed (not used)') + File.write(wc_podspec, podspec_text) + Pod::UI.puts "Patched react-native-compat.podspec: removed YttriumWrapper dependency".yellow + end +end + +files_removed = Dir.glob(File.join(wc_compat_dir, "ios", "RNWalletConnectPay*")).each do |f| + FileUtils.rm(f) +end +Pod::UI.puts "Removed WalletConnect Pay source files (not used)".yellow if files_removed.any? + +# ── Haptic feedback AudioToolbox fix ──────────────────────────────────────── +# react-native-haptic-feedback uses AudioServicesPlaySystemSound but its +# podspec doesn't declare the AudioToolbox framework. Patch it. +haptic_podspec = File.join(__dir__, "..", "node_modules", "react-native-haptic-feedback", "RNReactNativeHapticFeedback.podspec") +if File.exist?(haptic_podspec) + haptic_text = File.read(haptic_podspec) + if !haptic_text.include?("AudioToolbox") + haptic_text.gsub!(/s\.requires_arc\s*=\s*true/, "s.requires_arc = true\n s.frameworks = \"AudioToolbox\"") + File.write(haptic_podspec, haptic_text) + Pod::UI.puts "Patched RNReactNativeHapticFeedback.podspec: added AudioToolbox framework".yellow + end +end +# ───────────────────────────────────────────────────────────────────────────── + target "Self" do use_expo_modules! # Native module exclusion for E2E testing is handled in react-native.config.cjs diff --git a/app/ios/Podfile.lock b/app/ios/Podfile.lock index 994f7415e..03f139936 100644 --- a/app/ios/Podfile.lock +++ b/app/ios/Podfile.lock @@ -1499,7 +1499,6 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - YttriumWrapper (= 0.10.50) - react-native-get-random-values (1.11.0): - React-Core - react-native-netinfo (11.4.1): @@ -2344,7 +2343,7 @@ DEPENDENCIES: - RNSVG (from `../node_modules/react-native-svg`) - "SdkReactNative (from `../node_modules/@didit-protocol/sdk-react-native`)" - "segment-analytics-react-native (from `../node_modules/@segment/analytics-react-native`)" - - "SelfNFCPassportReader (from `git@github.com:selfxyz/NFCPassportReader.git`, commit `2cdc50a5c27b75594b94b27fdc4bb6172ada0f96`)" + - SelfNFCPassportReader (from `https://github.com/selfxyz/NFCPassportReader.git`, commit `2cdc50a5c27b75594b94b27fdc4bb6172ada0f96`) - "sovran-react-native (from `../node_modules/@segment/sovran-react-native`)" - SwiftQRScanner (from `https://github.com/vinodiOS/SwiftQRScanner`) - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) @@ -2605,7 +2604,7 @@ EXTERNAL SOURCES: :path: "../node_modules/@segment/analytics-react-native" SelfNFCPassportReader: :commit: 2cdc50a5c27b75594b94b27fdc4bb6172ada0f96 - :git: "git@github.com:selfxyz/NFCPassportReader.git" + :git: https://github.com/selfxyz/NFCPassportReader.git sovran-react-native: :path: "../node_modules/@segment/sovran-react-native" SwiftQRScanner: @@ -2616,7 +2615,7 @@ EXTERNAL SOURCES: CHECKOUT OPTIONS: SelfNFCPassportReader: :commit: 2cdc50a5c27b75594b94b27fdc4bb6172ada0f96 - :git: "git@github.com:selfxyz/NFCPassportReader.git" + :git: https://github.com/selfxyz/NFCPassportReader.git SwiftQRScanner: :commit: c71ff91297640a944de4bca61434155c3f9b0979 :git: https://github.com/vinodiOS/SwiftQRScanner @@ -2625,7 +2624,7 @@ SPEC CHECKSUMS: AppAuth: 1c1a8afa7e12f2ec3a294d9882dfa5ab7d3cb063 AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 - BVLinearGradient: cb006ba232a1f3e4f341bb62c42d1098c284da70 + BVLinearGradient: 880f91a7854faff2df62518f0281afb1c60d49a3 DiditSDK: 3113b1aa1e5f67e84e84bd8449273257e5d9eff0 DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb EXApplication: 4c72f6017a14a65e338c5e74fca418f35141e819 @@ -2661,112 +2660,112 @@ SPEC CHECKSUMS: GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 hermes-engine: 1f783c3d53940aed0d2c84586f0b7a85ab7827ef lottie-ios: 8f959969761e9c45d70353667d00af0e5b9cadb3 - lottie-react-native: d73a798e26348851f0ef349df3d40f2e27fd239b + lottie-react-native: 7f9c8ce134aeaa6530f8b78f8cabed4aab6b5a9a Mixpanel-swift: e9bef28a9648faff384d5ba6f48ecc2787eb24c0 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 OpenSSL-Universal: 84efb8a29841f2764ac5403e0c4119a28b713346 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 QKMRZParser: 6b419b6f07d6bff6b50429b97de10846dc902c29 QKMRZScanner: cf2348fd6ce441e758328da4adf231ef2b51d769 - RCT-Folly: e78785aa9ba2ed998ea4151e314036f6c49e6d82 + RCT-Folly: 36fe2295e44b10d831836cc0d1daec5f8abcf809 RCTDeprecation: f5c19ebdb8804b53ed029123eb69914356192fc8 RCTRequired: 6ae6cebe470486e0e0ce89c1c0eabb998e7c51f4 RCTTypeSafety: 50d6ec72a3d13cf77e041ff43a0617050fb98e3f React: e46fdbd82d2de942970c106677056f3bdd438d82 React-callinvoker: b027ad895934b5f27ce166d095ed0d272d7df619 - React-Core: 92733c8280b1642afed7ebfb3c523feaec946ece - React-CoreModules: e2dfd87b6fdb9d969b16871655885a4d89a2a9f4 - React-cxxreact: d1a70e78543bb5b159fdaf6c52cadd33c1ae3244 + React-Core: 36b7f20f655d47a35046e2b02c9aa5a8f1bcb61e + React-CoreModules: 7fac6030d37165c251a7bd4bde3333212544da3c + React-cxxreact: 0ead442ecaa248e7f71719e286510676495ae26d React-debug: c17d400ddcb2c45aa4f5efedeb443c72b58b40aa - React-defaultsnativemodule: af13e4f2629106aede1d6286921f852715017d64 - React-domnativemodule: b6785fc507cfcbdf24509a0be26fdac7454f7ea3 - React-Fabric: 5f8c48a36ff906a0e8761ff914ef368f67a25b59 - React-FabricComponents: 2ba16205b15ce80460a1dcc3725b3926493b47f8 - React-FabricImage: d1b0c203284c0ab077277a54830e4de4c0134908 + React-defaultsnativemodule: d8ddce2020fede6b0a6d3cccc3fbb1fedf1aab37 + React-domnativemodule: 17da9148ba917807b9bab6c4e1fddbc11303be64 + React-Fabric: fda27452bab6f8b5213f33c1d59a24f6c6b66579 + React-FabricComponents: 10623f84dcb5ae9b2bbe98f577546b10fa459fdb + React-FabricImage: 2237e1c2089eb4e55541485e173f96af43afca7d React-featureflags: 94805545eda554c548e3615f248f4f4c65ef279e - React-featureflagsnativemodule: 0ab7272372052fe9dc561dc2e4bbd4fd8ab11ea4 - React-graphics: 6800e73b337075ad0cb9226c1592ed1a91703244 - React-hermes: bf50c8272cb562300a54a621aa69dc12a0b4fcf2 - React-idlecallbacksnativemodule: 57d5b25440ed0478966710675354eac676508ff5 - React-ImageManager: fff4c0c50041d7b8f67d6f435e7a4b1e9125ad27 - React-jserrorhandler: 4abc5dfa7d5fb7bfba328faddfa97dc90441c276 - React-jsi: 19e77567e235d06b7e8f425d2a6c1e948ab286e9 - React-jsiexecutor: fe6ad8b9a2bf97e435fc1c969c80ed7f447ed68e - React-jsinspector: 01aa56b6037c65a6ec4432a120aa74cc6fdf514f - React-jsitracing: cb05a2c5c36eb212be028e26c38028f0d352c16b - React-logger: 02e5802824aa9b15cb7df42e10a91abead83cd8d - React-Mapbuffer: bbd3be71ef32e8198ac0f78b841662103e032ffe - React-microtasksnativemodule: 8e65fc37744388153b9bca94552d04955d852058 - react-native-app-auth: e21c8ee920876b960e38c9381971bd189ebea06b - react-native-biometrics: 43ed5b828646a7862dbc7945556446be00798e7d - react-native-blur: 745703f35133ed6a1210d4bbff358a631911f002 - react-native-cloud-storage: 796c793dc354bb49f9df27ca25eed0f79a15549e - react-native-compat: ad6a412f03632d1c4d97d47e56b22d0597116085 - react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba - react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187 - react-native-nfc-manager: c8891e460b4943b695d63f7f4effc6345bbefc83 - react-native-passkey: 3c07a93dc2608929d794b7298c0d29d01b379f01 - react-native-safe-area-context: bf457bf5b3a617e9a3930d1ecd954a3335303cc7 - react-native-sqlite-storage: 0c84826214baaa498796c7e46a5ccc9a82e114ed - react-native-webview: 05734d99f1e422c5ddfeefbd083d53abd78fccb1 + React-featureflagsnativemodule: b71dc56c26b09c5becaabc59d90eb6715a76d01e + React-graphics: f81c5369a01264f5e5f2ab7b2e7fbe769c94ed42 + React-hermes: 13e1c1c9222503bcd7ad450370c5a26dc9b46ebe + React-idlecallbacksnativemodule: 16c2ade55cf3537f7d6d1afb7acb230d65b1d63c + React-ImageManager: 130248847aada2e9485db30cef63284ffc2f0846 + React-jserrorhandler: ef0948d6835b991094660d93cb7dcf3446d065f5 + React-jsi: 931610846e52e5d157f4bc3f71a14f9a53573abd + React-jsiexecutor: 3f5fb21d47c5c72c13a1710b288d78c8209a38f9 + React-jsinspector: 231977808d975ea2ad045b910623651ef7219657 + React-jsitracing: 9b717dd9c91915ccf51af10df94e8c38de722786 + React-logger: 9a0c4e1e41cd640ac49d69aacadab783f7e0096b + React-Mapbuffer: 257e617e7554c0ec448d13d38b13ee3cbdd3c5eb + React-microtasksnativemodule: fa9db75d61e2053274057767ced1a2e2c485b0fa + react-native-app-auth: 9b0a0e3ca279c3426a451e2607c8483808b8ed4a + react-native-biometrics: 352e5a794bfffc46a0c86725ea7dc62deb085bdc + react-native-blur: 6782cb12b39a0200ad2a782fb9a5529c2c83c33b + react-native-cloud-storage: 8dc640aac2cf6e8a6231cc49696e8f8405b716bf + react-native-compat: ef7486ca6f41481467445f4e0fd997d42a84460f + react-native-get-random-values: 21325b2244dfa6b58878f51f9aa42821e7ba3d06 + react-native-netinfo: f0a9899081c185db1de5bb2fdc1c88c202a059ac + react-native-nfc-manager: ef3b44c4f1975ab16d6109bb1671ab68068aba58 + react-native-passkey: 8a3ecd4c44e020323841b9d90e779e9dd9e1db27 + react-native-safe-area-context: f73c45199e5df289e0655c8fceb4e6f4fcfab256 + react-native-sqlite-storage: f6d515e1c446d1e6d026aa5352908a25d4de3261 + react-native-webview: de5205a97121427588aff27de2ddea4cc9fc0a19 React-nativeconfig: 334c9961d74ddd3bc203afb92ee574ed01c7c755 - React-NativeModulesApple: bf996c9e3b86e579e6e8635633b721c165a60b2c - React-perflogger: 721172bda31a65ce7b7a0c3bf3de96f12ef6f45d - React-performancetimeline: a23bfc89694e13ead855f25049bb9d60ce3704a2 + React-NativeModulesApple: e55f72e014482edd711542815a98b865ee6de9a1 + React-perflogger: 15a7bcb6c46eae8a981f7add8c9f4172e2372324 + React-performancetimeline: 9fb03db27775ddef6a98e3d22811acf210f07ba4 React-RCTActionSheet: 25eb72eabade4095bfaf6cd9c5c965c76865daa8 - React-RCTAnimation: 8efbd0a4a71fd3dbe84e6d08b92bec5728b7524b - React-RCTAppDelegate: 8ff6da817adefd15d4e25ade53a477c344f9b213 - React-RCTBlob: 6056bd62a56a6d2dad55cdf195949db1de623e14 - React-RCTFabric: 113fe8b6532ac21a6a46700b2650b8d458020ee4 - React-RCTFBReactNativeSpec: 4214925b1c4829fb1e73bfbacb301244b522dc11 - React-RCTImage: 7b3f38c77e183bdcb43dbcd7b5842b96c814889a - React-RCTLinking: 6cca74db71b23f670b72e45603e615c2b72b2235 - React-RCTNetwork: 5791b0718eff20c12f6f3d62e2ad50cff4b5c8a0 - React-RCTSettings: 84154e31a232b5b03b6b7a89924a267c431ccf16 - React-RCTText: cd49cb4442ee7f64b0415b27745d2495cb40cfaa - React-RCTVibration: 2a7432e61d42f802716bd67edc793b5e5f58971a + React-RCTAnimation: 04c987fa858fa16169f543d29edb4140bd35afa9 + React-RCTAppDelegate: b2707904e4f8ad92fd052e62684bf0c3b88381cc + React-RCTBlob: 1f214a7211632515805dd1f1b81fac70d12f812d + React-RCTFabric: 0838a13e11c221d1d5648257b2ca31fede22874b + React-RCTFBReactNativeSpec: 60d72b45a150ca35748b9a77028674b1e56a2e43 + React-RCTImage: e516d72739797fb7c1dac5c691f02a0f5445c290 + React-RCTLinking: 1e5554afe4f959696ad3285738c1510f2592f220 + React-RCTNetwork: 65e1e52c8614dcab342fa1eaec750ca818160e74 + React-RCTSettings: e86c204b481ef9264929fe00d1fdd04ce561748a + React-RCTText: 15f14d6f9b75e64ffe749c75e30ff047cf0fa1be + React-RCTVibration: 8d9078d5432972fe12d9f1526b38f504ad3d45cb React-rendererconsistency: 9da9009da0eafdf005a77a260b1dbea274a90aa8 - React-rendererdebug: 4b9e70532888e08f41c5fcbcbc050e99a590839c + React-rendererdebug: bb56856ce3901396c959ddcf0991f7a3a162f4c5 React-rncore: d380e5c97ec669c0bd097612cd98247597a32679 - React-RuntimeApple: 0088247d510e7eb4a3a2ecc0964411266730d10d - React-RuntimeCore: 0e45d29ad4057b029db38e92ab24d4294253c6e3 + React-RuntimeApple: 559b3d8f068335e896224b8365fd8cee814e6652 + React-RuntimeCore: 87c25d97233f61b68bb254360e2724c01eb93198 React-runtimeexecutor: f9ae11481be048438640085c1e8266d6afebae44 - React-RuntimeHermes: 4d6bbb8c4832794c34fc2a0301a885a9e8c936d5 - React-runtimescheduler: bb1282886aa8ba594ff5704c14ba19af1551149f + React-RuntimeHermes: 0cba4a2b329dcb8392754dd20a839709c7e3389f + React-runtimescheduler: 62d73526c3471884a896328e11a930ea4b42dfe1 React-timing: 9b94f0fb713587a697ce56b0fc7cb31cb5be70a5 - React-utils: 07c3365e9dcbb8940e912ce099b20fb0e56dbacf - ReactAppDependencyProvider: 6e8d68583f39dc31ee65235110287277eb8556ef - ReactCodegen: 58a974a1a86362975fd49596480c5f0f17ee06a2 - ReactCommon: e686c5766f0ebe5293be5a3957b833645cdac8ad - RNAppleAuthentication: a89c9804592b38ed4ab11f0aee68d05ba12ad432 - RNCAsyncStorage: 6a8127b6987dc9fbce778669b252b14c8355c7ce - RNCClipboard: 9f7b908de4bf4353871fb454c15fc03db4917b88 - RNDeviceInfo: 4c852998208b60dc192ae3529e5867817719ad1e - RNFBAnalytics: 03c83ba4617a3754c99e66267983efcc908932a9 - RNFBApp: a448037d2df74af9d374a0b765be12ff1e844dc0 - RNFBMessaging: 0f0498a95c605e3afcf13ac5f349d0b201ea65f6 - RNFBRemoteConfig: 4eb5fc9f21dc324153c7d3f5b48c935ab9031876 - RNGestureHandler: 36aca36e4ef19f55dbf97239199d00fd58494e34 - RNGoogleSignin: 60c3f470558dbff0ae54f2f164ef82a89d3eb561 - RNInAppBrowser: 904d24dc75e8e6c6c98a3160329192608946f9df - RNKeychain: 35beaa17938f7d8e4990d8a38fad5f8a748fc47c - RNLocalize: aa57bee9fcd545b98ce773a8e2404f9a36115b4a - RNReactNativeHapticFeedback: eb5395b503c7a8f10de5e6722ef8afd3c61bc4f5 - RNScreens: b0811b109e1a0b8b579f3348018e177bee374840 - RNSentry: 98ab9f6a16c9596e36565ccf1a5871323f334766 - RNSVG: d926926b169d8b81eb06aeb69734076e1dd566a3 - SdkReactNative: 34ba85b3f3060892c548b7415b06f0bd66fcba1c - segment-analytics-react-native: 8ab9c49df1859bbd6be93cf90a91ade17f20a0aa + React-utils: 9e73840482020d1914b68089e807b3f2f56b10a3 + ReactAppDependencyProvider: 3d947e9d62f351c06c71497e1be897e6006dc303 + ReactCodegen: e92a1659b32705bd8ee0d2ba016d6993a4ade05b + ReactCommon: a02340b2a1a76f3703298a4680bb03277ca87440 + RNAppleAuthentication: d6fe579e5f43cf8db54bdc48518bccea61c592a4 + RNCAsyncStorage: 481acf401089f312189e100815088ea5dafc583c + RNCClipboard: 4d8c76e488f1491e5235901b7028ff53a678bd94 + RNDeviceInfo: d79872e11c8e9c4de0d65b0ee6e0cee719f37fea + RNFBAnalytics: caec446c723d33cfdcf75aab53fa1287e499b2d0 + RNFBApp: fda4a8b08fe31bea8492808106aa638d1bb595b7 + RNFBMessaging: 099972ab397d61815f32610514b0573d1db2b1e1 + RNFBRemoteConfig: ce28355c29a432ac29c9a9d10816a906c0fee938 + RNGestureHandler: 75a1894590b15c560094c2b09c5dce6a64eefa29 + RNGoogleSignin: bd5e55072fc89c69e3eb139be2a9c8935d0a0f2c + RNInAppBrowser: b53e6f6072c931115bc22ac9dc9510ac2cbea62d + RNKeychain: 774184659ed098fd715a4976d44e2003c829934f + RNLocalize: af6fb26fab9def1513da0afacc1cb6781200871e + RNReactNativeHapticFeedback: 5e0b136ae9fa95f0227ef5d6216d732f680d2904 + RNScreens: cc97e4382039563c725394067185356352df69ad + RNSentry: f343c58d33eb8351a5b5cfbb157d3527e2f59645 + RNSVG: 2b1b9e597b2a0847e2963aefe17d976d5c882f3f + SdkReactNative: bec23c7789377abaf8218494b3b2e41a690c2c07 + segment-analytics-react-native: ab16aeb1731acc05670dcec1aaed13b6bcbc1b6d SelfNFCPassportReader: 8b53f9d483e0dd1f1a275953e3dc6dfc733694c5 Sentry: 59993bffde4a1ac297ba6d268dc4bbce068d7c1b SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 - sovran-react-native: a3ad3f8ff90c2002b2aa9790001a78b0b0a38594 + sovran-react-native: eec37f82e4429f0e3661f46aaf4fcd85d1b54f60 SwiftQRScanner: e85a25f9b843e9231dab89a96e441472fe54a724 SwiftyTesseract: 1f3d96668ae92dc2208d9842c8a59bea9fad2cbb Yoga: c34725819ab0a5962e85455b9e56679b306910ee YttriumWrapper: d7f63336830536f1da41b745ed8bacedb04228c4 ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5 -PODFILE CHECKSUM: 83f631d1b6308502a035e656b2f9dfab30431ae3 +PODFILE CHECKSUM: 52fe8b0692d175ef85e813596606f198bb5d3728 COCOAPODS: 1.16.2 From 3e4faef24a0460d874028d7183bd9c78f5f4290f Mon Sep 17 00:00:00 2001 From: turnoffthiscomputer <98749896+remicolin@users.noreply.github.com> Date: Fri, 17 Apr 2026 15:55:31 +0200 Subject: [PATCH 07/15] feat(ci): add Android PR preview builds via Internal App Sharing (#1984) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a `pr_number` input to `mobile-deploy.yml`. When set, the Android pipeline uploads the AAB to Play Store Internal App Sharing (unique download URL per upload) instead of promoting to the internal track, and posts a Slack message to `SLACK_WEBHOOK_QA_BUILDS` with the install link and PR metadata. The version-bump PR job is skipped and iOS is always skipped in this mode. Enables an agent flow: open PR to `dev` → `gh workflow run` → reviewer taps Slack link to QA on device → merge. - `upload_to_play_store.py`: new `--mode=ias` branch using `internalappsharingartifacts.uploadbundle`; exports `download_url` to `$GITHUB_OUTPUT`. - `mobile-deploy.yml`: new `workflow_dispatch` input `pr_number`; concurrency keyed per-PR so parallel previews don't queue; branch between IAS and track upload; Slack notify step on success. --- .github/workflows/mobile-deploy.yml | 125 ++++++++++++++++++++++++++-- app/scripts/upload_to_play_store.py | 65 ++++++++++++++- 2 files changed, 180 insertions(+), 10 deletions(-) diff --git a/.github/workflows/mobile-deploy.yml b/.github/workflows/mobile-deploy.yml index 6b17ae02b..73e94e781 100644 --- a/.github/workflows/mobile-deploy.yml +++ b/.github/workflows/mobile-deploy.yml @@ -97,6 +97,11 @@ on: required: false type: boolean default: false + pr_number: + description: "PR number for per-PR preview build. When set (Android only), uploads to Play Store Internal App Sharing and posts a Slack message with the install URL. Skips the version-bump PR." + required: false + type: string + default: "" pull_request: types: [closed] @@ -131,10 +136,11 @@ on: default: false concurrency: - # Group by deployment track or ref name to allow different tracks to run in parallel - # cancel-in-progress: false ensures we don't cancel ongoing deployments - # Branch-locking in create-version-bump-pr prevents duplicate PRs for same version - group: mobile-deploy-${{ inputs.deployment_track || github.ref_name }} + # Group by PR number (when set) so multiple PR preview builds run in parallel, + # otherwise by deployment track or ref name to allow different tracks to run in parallel. + # cancel-in-progress: false ensures we don't cancel ongoing deployments. + # Branch-locking in create-version-bump-pr prevents duplicate PRs for same version. + group: mobile-deploy-${{ inputs.pr_number != '' && format('pr-{0}', inputs.pr_number) || inputs.deployment_track || github.ref_name }} cancel-in-progress: false jobs: @@ -263,7 +269,9 @@ jobs: permissions: contents: read actions: write + # PR preview builds are Android-only for now — always skip iOS when pr_number is set. if: | + inputs.pr_number == '' && (github.event_name != 'pull_request' || (github.event.action == 'closed' && github.event.pull_request.merged == true)) && ( @@ -1236,8 +1244,20 @@ jobs: if: inputs.platform != 'ios' uses: ./.github/actions/cleanup-gradle-artifacts - - name: Upload to Google Play Store using WIF - if: inputs.platform != 'ios' && inputs.test_mode != true + - name: Upload to Play Store Internal App Sharing (PR preview) + id: ias-upload + if: inputs.platform != 'ios' && inputs.test_mode != true && inputs.pr_number != '' + timeout-minutes: 10 + run: | + cd ${{ env.APP_PATH }} + echo "🧪 PR preview build for #${{ inputs.pr_number }} — uploading to Internal App Sharing..." + python scripts/upload_to_play_store.py \ + --aab "android/app/build/outputs/bundle/release/app-release.aab" \ + --package-name "${{ secrets.ANDROID_PACKAGE_NAME }}" \ + --mode ias + + - name: Upload to Google Play Store track using WIF + if: inputs.platform != 'ios' && inputs.test_mode != true && inputs.pr_number == '' timeout-minutes: 10 run: | cd ${{ env.APP_PATH }} @@ -1251,6 +1271,96 @@ jobs: --package-name "${{ secrets.ANDROID_PACKAGE_NAME }}" \ --track "$DEPLOYMENT_TRACK" + - name: Post PR preview build link to Slack + if: inputs.platform != 'ios' && inputs.test_mode != true && inputs.pr_number != '' && steps.ias-upload.outputs.download_url != '' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_QA_BUILDS }} + PR_NUMBER: ${{ inputs.pr_number }} + DOWNLOAD_URL: ${{ steps.ias-upload.outputs.download_url }} + APP_VERSION: ${{ needs.bump-version.outputs.version }} + BUILD_NUMBER: ${{ needs.bump-version.outputs.android_build }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + if [ -z "$SLACK_WEBHOOK_URL" ]; then + echo "⚠️ SLACK_WEBHOOK_QA_BUILDS not configured — skipping Slack notify" + echo " (Install URL: $DOWNLOAD_URL)" + exit 0 + fi + + # Fetch PR metadata so the Slack card is informative + PR_JSON=$(gh api "repos/${{ github.repository }}/pulls/${PR_NUMBER}" --jq '{title,html_url,user:.user.login,body}') + PR_TITLE=$(echo "$PR_JSON" | python3 -c 'import json,sys;print(json.load(sys.stdin)["title"])') + PR_URL=$(echo "$PR_JSON" | python3 -c 'import json,sys;print(json.load(sys.stdin)["html_url"])') + PR_USER=$(echo "$PR_JSON" | python3 -c 'import json,sys;print(json.load(sys.stdin)["user"])') + + python3 <<'PY' + import json, os, urllib.request + + payload = { + "text": f"Android preview build ready for PR #{os.environ['PR_NUMBER']}", + "blocks": [ + { + "type": "header", + "text": {"type": "plain_text", "text": f"🤖 Android preview — PR #{os.environ['PR_NUMBER']}"}, + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": f"*<{os.environ['PR_URL']}|{os.environ.get('PR_TITLE','(no title)')}>*\n_by {os.environ.get('PR_USER','?')}_", + }, + }, + { + "type": "section", + "fields": [ + {"type": "mrkdwn", "text": f"*Version*\n`{os.environ.get('APP_VERSION','?')} ({os.environ.get('BUILD_NUMBER','?')})`"}, + {"type": "mrkdwn", "text": f"*Source*\n<{os.environ['RUN_URL']}|CI run>"}, + ], + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "style": "primary", + "text": {"type": "plain_text", "text": "📱 Install on Android"}, + "url": os.environ["DOWNLOAD_URL"], + }, + { + "type": "button", + "text": {"type": "plain_text", "text": "View PR"}, + "url": os.environ["PR_URL"], + }, + ], + }, + { + "type": "context", + "elements": [ + {"type": "mrkdwn", "text": "Open the install link on your Android device (must be signed into a Play Store tester account)."}, + ], + }, + ], + } + + req = urllib.request.Request( + os.environ["SLACK_WEBHOOK_URL"], + data=json.dumps(payload).encode(), + headers={"Content-Type": "application/json"}, + ) + with urllib.request.urlopen(req) as resp: + print(f"Slack webhook status: {resp.status}") + PY + + # Also export to the run summary so the URL is visible in the GH Actions UI + { + echo "## 📱 Android preview build" + echo "" + echo "- PR: [#$PR_NUMBER]($PR_URL) — $PR_TITLE" + echo "- Version: \`$APP_VERSION ($BUILD_NUMBER)\`" + echo "- Install URL: $DOWNLOAD_URL" + } >> "$GITHUB_STEP_SUMMARY" + - name: Monitor cache usage if: always() run: | @@ -1290,8 +1400,11 @@ jobs: contents: write pull-requests: write needs: [bump-version, build-ios, build-android] + # Skip when this is a per-PR preview build — preview builds must not mutate + # the repo's version lineage. if: | always() && + inputs.pr_number == '' && (github.event_name != 'pull_request' || (github.event.action == 'closed' && github.event.pull_request.merged == true)) && (needs.build-ios.result == 'success' || needs.build-android.result == 'success') diff --git a/app/scripts/upload_to_play_store.py b/app/scripts/upload_to_play_store.py index e22185b73..d4ba2a7d4 100644 --- a/app/scripts/upload_to_play_store.py +++ b/app/scripts/upload_to_play_store.py @@ -79,6 +79,56 @@ def should_hold_for_manual_review(track): return track == 'production' +def upload_to_internal_app_sharing(aab_path, package_name, credentials): + """Upload AAB to Google Play Internal App Sharing. + + Returns a unique downloadUrl per upload. Designed for per-PR/per-build + preview distribution: does NOT advance any track, does NOT require a + unique versionCode, and does NOT go through review. + """ + print(f"📤 Uploading {aab_path} to Internal App Sharing...") + + try: + service = build('androidpublisher', 'v3', credentials=credentials) + + media = MediaFileUpload(aab_path, mimetype='application/octet-stream') + request = service.internalappsharingartifacts().uploadbundle( + packageName=package_name, + media_body=media, + ) + response = request.execute() + + download_url = response.get('downloadUrl') + sha256 = response.get('sha256') + cert_fingerprint = response.get('certificateFingerprint') + + if not download_url: + print("❌ IAS upload returned no downloadUrl") + print(f"Response: {response}") + return False + + print(f"✅ Uploaded to Internal App Sharing") + print(f"🔗 downloadUrl: {download_url}") + if sha256: + print(f"🔐 sha256: {sha256}") + if cert_fingerprint: + print(f"📜 certificateFingerprint: {cert_fingerprint}") + + # Expose the URL to GitHub Actions via $GITHUB_OUTPUT when available + github_output = os.environ.get('GITHUB_OUTPUT') + if github_output: + with open(github_output, 'a') as f: + f.write(f"download_url={download_url}\n") + if sha256: + f.write(f"sha256={sha256}\n") + + return True + + except Exception as e: + print(f"❌ IAS upload failed: {e}") + return False + + def upload_to_play_store(aab_path, package_name, track, credentials): """Upload AAB to Google Play Store""" print(f"📤 Uploading {aab_path} to Play Store...") @@ -160,7 +210,9 @@ def main(): parser = argparse.ArgumentParser(description='Upload Android AAB to Google Play Store using WIF') parser.add_argument('--aab', required=True, help='Path to the AAB file') parser.add_argument('--package-name', required=True, help='Android package name') - parser.add_argument('--track', default='internal', help='Release track (internal, alpha, beta, production)') + parser.add_argument('--track', default='internal', help='Release track (internal, alpha, beta, production). Ignored when --mode=ias.') + parser.add_argument('--mode', default='track', choices=['track', 'ias'], + help='Upload mode: "track" promotes to a Play Store track; "ias" uploads to Internal App Sharing and returns a unique downloadUrl.') args = parser.parse_args() @@ -170,15 +222,20 @@ def main(): print(f"❌ Error: AAB file not found: {aab_path}") sys.exit(1) - print("🚀 Starting Google Play Store upload with Workload Identity Federation") + print("🚀 Starting Google Play upload with Workload Identity Federation") print(f"📦 AAB: {aab_path}") print(f"📱 Package: {args.package_name}") - print(f"🎯 Track: {args.track}") + print(f"🧭 Mode: {args.mode}") + if args.mode == 'track': + print(f"🎯 Track: {args.track}") print() # Get credentials and upload credentials = get_credentials() - success = upload_to_play_store(str(aab_path), args.package_name, args.track, credentials) + if args.mode == 'ias': + success = upload_to_internal_app_sharing(str(aab_path), args.package_name, credentials) + else: + success = upload_to_play_store(str(aab_path), args.package_name, args.track, credentials) if success: print("\n🎉 Upload completed successfully!") From e2daa9715179ed6ec029fd36e558b924055b6a27 Mon Sep 17 00:00:00 2001 From: "Seshanth.S" <35675963+seshanthS@users.noreply.github.com> Date: Sat, 18 Apr 2026 00:44:55 +0530 Subject: [PATCH 08/15] Enable logs (#1985) * enable logs * improve NFC observability logging and add session-scoped log grouping * Enhance logging by adding session ID to lokiTransport for improved traceability --- app/ios/Podfile | 2 +- app/src/services/logging/logger/lokiTransport.ts | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app/ios/Podfile b/app/ios/Podfile index a1b0d67ee..731870335 100755 --- a/app/ios/Podfile +++ b/app/ios/Podfile @@ -144,7 +144,7 @@ target "Self" do nfc_repo_url = "git@github.com:selfxyz/NFCPassportReader.git" end - pod "SelfNFCPassportReader", git: nfc_repo_url, commit: "2cdc50a5c27b75594b94b27fdc4bb6172ada0f96" + pod "SelfNFCPassportReader", git: nfc_repo_url, commit: "b478e1f1320e86f72c5755bc5adf156fff950585" end # Explicitly declare Mixpanel to ensure it's available even in E2E builds diff --git a/app/src/services/logging/logger/lokiTransport.ts b/app/src/services/logging/logger/lokiTransport.ts index c21d76c84..6446e6edf 100644 --- a/app/src/services/logging/logger/lokiTransport.ts +++ b/app/src/services/logging/logger/lokiTransport.ts @@ -29,6 +29,9 @@ interface LokiPayload { streams: LokiStream[]; } +// Per-session ID for grouping logs in Grafana (not persistent, not user-identifiable) +const sessionId = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`; + // Batch management state let batch: LokiLogEntry[] = []; let batchTimer: NodeJS.Timeout | null = null; @@ -63,6 +66,7 @@ const sendBatch = async ( app: 'self-mobile', platform: 'react-native', level, + session_id: sessionId, }, values, }), @@ -88,12 +92,11 @@ const sendBatch = async ( }); if (!response.ok) { - console.warn( - `Loki transport failed: ${response.status} ${response.statusText}`, - ); + // Silently fail — console.warn here would trigger consoleInterceptor + // feedback loop when logger is enabled in dev } - } catch (error) { - console.warn('Loki transport error:', error); + } catch { + // Silently fail — same feedback loop risk } }; @@ -195,11 +198,13 @@ const lokiTransport: transportFunctionType = props => { level: string; message: string; timestamp: string; + session_id: string; data?: unknown; } = { level: level.text, message: actualMessage, timestamp, + session_id: sessionId, }; if (actualData) { From 043b0558faa788724270fe9c47978b3a2eae1eff Mon Sep 17 00:00:00 2001 From: turnoffthiscomputer <98749896+remicolin@users.noreply.github.com> Date: Fri, 17 Apr 2026 22:36:07 +0200 Subject: [PATCH 09/15] Fix recovery phrase screen wonkiness (SELF-2649) (#1983) * Fix recovery phrase screen wonkiness (SELF-2649) - Swap paste XStack to Pressable with hitSlop to fix multiple-tap issue - Add error state + user-facing messages for all failure modes - Dismiss keyboard on Continue press - Clear error on new input * Format RecoverWithPhraseScreen with Prettier * Clear error state at start of restoreAccount --------- Co-authored-by: Agent PM --- .../recovery/RecoverWithPhraseScreen.tsx | 71 +++++++++++++++---- 1 file changed, 57 insertions(+), 14 deletions(-) diff --git a/app/src/screens/account/recovery/RecoverWithPhraseScreen.tsx b/app/src/screens/account/recovery/RecoverWithPhraseScreen.tsx index 45760c2e1..0d399d5c3 100644 --- a/app/src/screens/account/recovery/RecoverWithPhraseScreen.tsx +++ b/app/src/screens/account/recovery/RecoverWithPhraseScreen.tsx @@ -4,7 +4,7 @@ import { ethers } from 'ethers'; import React, { useCallback, useState } from 'react'; -import { Keyboard, StyleSheet } from 'react-native'; +import { Keyboard, Pressable, StyleSheet } from 'react-native'; import { Text, TextArea, View, XStack, YStack } from 'tamagui'; import Clipboard from '@react-native-clipboard/clipboard'; import { useNavigation } from '@react-navigation/native'; @@ -22,6 +22,7 @@ import { import { BackupEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; import { black, + red500, slate300, slate400, slate600, @@ -38,6 +39,22 @@ import { } from '@/providers/passportDataProvider'; import { recoveryCopy } from '@/screens/account/recovery/recoveryCopy'; +type RecoveryError = + | 'invalid_mnemonic' + | 'restore_failed' + | 'not_registered' + | 'unexpected_error'; + +const ERROR_MESSAGES: Record = { + invalid_mnemonic: + 'That doesn’t look like a valid recovery phrase. Make sure all 24 words are correct and in the right order.', + restore_failed: + 'We couldn’t restore your account with this phrase. Please double-check and try again.', + not_registered: + 'This recovery phrase doesn’t match a registered ID. If you registered with a different phrase, try that one instead.', + unexpected_error: 'Something went wrong. Please try again.', +}; + const RecoverWithPhraseScreen: React.FC = () => { const navigation = useNavigation>(); @@ -47,20 +64,31 @@ const RecoverWithPhraseScreen: React.FC = () => { const { trackEvent } = useSelfClient(); const [mnemonic, setMnemonic] = useState(); const [restoring, setRestoring] = useState(false); + const [error, setError] = useState(null); + const onPaste = useCallback(async () => { const clipboard = (await Clipboard.getString()).trim(); // bugfix: perform a simple clipboard check; ethers.Mnemonic.isValidMnemonic doesn't work if (clipboard) { setMnemonic(clipboard); + setError(null); Keyboard.dismiss(); } }, []); + const onChangeText = useCallback((text: string) => { + setMnemonic(text); + setError(null); + }, []); + const restoreAccount = useCallback(async () => { + Keyboard.dismiss(); + setError(null); try { setRestoring(true); const slimMnemonic = mnemonic?.trim(); if (!slimMnemonic || !ethers.Mnemonic.isValidMnemonic(slimMnemonic)) { + setError('invalid_mnemonic'); setRestoring(false); return; } @@ -71,6 +99,7 @@ const RecoverWithPhraseScreen: React.FC = () => { trackEvent(BackupEvents.CLOUD_RESTORE_FAILED_AUTH, { mnemonicLength: slimMnemonic.split(' ').length, }); + setError('restore_failed'); setRestoring(false); return; } @@ -121,6 +150,7 @@ const RecoverWithPhraseScreen: React.FC = () => { reason: 'document_not_registered', hasCSCA: !!csca, }); + setError('not_registered'); setRestoring(false); return; } @@ -138,6 +168,7 @@ const RecoverWithPhraseScreen: React.FC = () => { reason: 'unexpected_error', error: error instanceof Error ? error.message : 'unknown', }); + setError('unexpected_error'); setRestoring(false); } }, [ @@ -161,7 +192,7 @@ const RecoverWithPhraseScreen: React.FC = () => {