From 1b4f12f57baebf0806081583e4f70a0de991ea40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 10 Jul 2026 13:42:32 +0200 Subject: [PATCH 1/2] feat(mobile): replace Support tile with Feedback flow Tapping Feedback asks how the user likes the app. Happy path prompts for a store review (native in-app popup first time, store deep link after); unhappy path routes to the existing support email composer. --- apps/mobile/package.json | 1 + apps/mobile/src/components/profile-screen.tsx | 28 ++---- apps/mobile/src/lib/feedback.ts | 91 +++++++++++++++++++ apps/mobile/src/lib/storage-keys.ts | 1 + pnpm-lock.yaml | 14 +++ 5 files changed, 113 insertions(+), 22 deletions(-) create mode 100644 apps/mobile/src/lib/feedback.ts diff --git a/apps/mobile/package.json b/apps/mobile/package.json index d12117a724..de885b7117 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -66,6 +66,7 @@ "expo-sharing": "~55.0.21", "expo-splash-screen": "55.0.22", "expo-status-bar": "55.0.6", + "expo-store-review": "~55.0.15", "expo-tracking-transparency": "55.0.15", "expo-web-browser": "55.0.17", "jotai": "2.18.1", diff --git a/apps/mobile/src/components/profile-screen.tsx b/apps/mobile/src/components/profile-screen.tsx index 5cd6dfc67a..10ecfb87f5 100644 --- a/apps/mobile/src/components/profile-screen.tsx +++ b/apps/mobile/src/components/profile-screen.tsx @@ -4,13 +4,13 @@ import { type Href, useRouter } from 'expo-router'; import { GitPullRequest, KeyRound, - LifeBuoy, Lock, LogOut, + MessageSquare, ShieldCheck, Trash2, } from 'lucide-react-native'; -import { Alert, Linking, Platform, Pressable, ScrollView, View } from 'react-native'; +import { Alert, Platform, Pressable, ScrollView, View } from 'react-native'; import { toast } from 'sonner-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import Animated, { FadeIn, FadeOut, LinearTransition } from 'react-native-reanimated'; @@ -23,6 +23,7 @@ import { ConfigureRow } from '@/components/ui/configure-row'; import { Skeleton } from '@/components/ui/skeleton'; import { Text } from '@/components/ui/text'; import { useAuth } from '@/lib/auth/auth-context'; +import { showFeedbackPrompt } from '@/lib/feedback'; import { useCurrentUserId } from '@/lib/hooks/use-current-user-id'; import { useThemeColors } from '@/lib/hooks/use-theme-colors'; import { useOrganization } from '@/lib/organization-context'; @@ -31,8 +32,6 @@ import { getSecurityAgentPath } from '@/lib/security-agent'; import { getTabBarOverlayHeight } from '@/lib/tab-bar-layout'; import { useTRPC } from '@/lib/trpc'; -const SUPPORT_EMAIL = 'hi@kilo.ai'; - function providerIcon(_provider: string) { return KeyRound; } @@ -97,21 +96,6 @@ export function ProfileScreen() { const { bottom } = useSafeAreaInsets(); - const openSupportEmail = async () => { - const envDetails = [ - `User ID: ${userId ?? 'unknown'}`, - `App version: ${Application.nativeApplicationVersion} (${Application.nativeBuildVersion})`, - `OS: ${Platform.OS} ${Platform.Version}`, - ].join('\n'); - const body = `\n\n---\n${envDetails}`; - const url = `mailto:${SUPPORT_EMAIL}?subject=${encodeURIComponent('mobile app feedback')}&body=${encodeURIComponent(body)}`; - try { - await Linking.openURL(url); - } catch { - toast.error(`No email app available. You can reach us at ${SUPPORT_EMAIL}`); - } - }; - const deleteAccount = useMutation( trpc.user.requestAccountDeletion.mutationOptions({ onSuccess: () => { @@ -262,11 +246,11 @@ export function ProfileScreen() { { - void openSupportEmail(); + showFeedbackPrompt(userId); }} /> false))) { + await SecureStore.setItemAsync(REVIEW_REQUESTED_AT_KEY, new Date().toISOString()); + try { + await StoreReview.requestReview(); + return; + } catch { + // Native popup failed — fall through to the store page. + } + } + try { + await Linking.openURL(STORE_REVIEW_URL); + } catch { + toast.error('Could not open the store.'); + } +} + +export function showFeedbackPrompt(userId: string | undefined) { + Alert.alert('How are you liking the Kilo app?', undefined, [ + { text: 'Cancel', style: 'cancel' }, + { + text: 'I like it', + onPress: () => { + Alert.alert( + "We're glad to hear that!", + 'A store review would help us out immensely. Thanks for using Kilo!', + [ + { text: 'Not now', style: 'cancel' }, + { + text: 'Rate Kilo', + onPress: () => { + void rateApp(); + }, + }, + ] + ); + }, + }, + { + text: 'Needs work', + onPress: () => { + Alert.alert( + "We're sorry to hear that!", + 'Please let us know what needs to be better. The engineer in charge of the app reads every single report!', + [ + { text: 'Not now', style: 'cancel' }, + { + text: 'Email us', + onPress: () => { + void openSupportEmail(userId); + }, + }, + ] + ); + }, + }, + ]); +} diff --git a/apps/mobile/src/lib/storage-keys.ts b/apps/mobile/src/lib/storage-keys.ts index 541feac18f..c33ee361fa 100644 --- a/apps/mobile/src/lib/storage-keys.ts +++ b/apps/mobile/src/lib/storage-keys.ts @@ -14,3 +14,4 @@ export const LAST_ACTIVE_INSTANCE_KEY = 'last-active-chat-instance'; export const CONSENT_USER_KEY_PREFIX = 'consent-accepted-'; export const AGENT_MODEL_PREFERENCE_KEY = 'agent-model-preference'; export const REASONING_DEFAULT_EXPANDED_KEY = 'agent-reasoning-default-expanded'; +export const REVIEW_REQUESTED_AT_KEY = 'store-review-requested-at'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6a6eecf9bd..d636bae5cf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -387,6 +387,9 @@ importers: expo-status-bar: specifier: 55.0.6 version: 55.0.6(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0) + expo-store-review: + specifier: ~55.0.15 + version: 55.0.15(expo@55.0.27)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6)) expo-tracking-transparency: specifier: 55.0.15 version: 55.0.15(expo@55.0.27)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6)) @@ -12177,6 +12180,12 @@ packages: react: '*' react-native: '*' + expo-store-review@55.0.15: + resolution: {integrity: sha512-p4hnMPMqP/CzX9rI32dI8xwWzIAXYQgCldUpx/me/xA3SBwQhjC1OzaCvmtHolxpVv19ut9YtxH2JXTAoadrVw==} + peerDependencies: + expo: '*' + react-native: '*' + expo-symbols@55.0.9: resolution: {integrity: sha512-F85C/8ExQjd2gYjasLVKMT8wPj+1+19TVTqg4jAeVjVZklqiQtLO72io9Ji1xAjYNgmDeUI0diVHlFMMTC4Ekg==} peerDependencies: @@ -28883,6 +28892,11 @@ snapshots: react-native: 0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6) react-native-is-edge-to-edge: 1.3.1(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0) + expo-store-review@55.0.15(expo@55.0.27)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6)): + dependencies: + expo: 55.0.27(@babel/core@7.29.0)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(bufferutil@4.1.0)(expo-router@55.0.16)(react-dom@19.2.6(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + react-native: 0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6) + expo-symbols@55.0.9(expo-font@55.0.8)(expo@55.0.27)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0): dependencies: '@expo-google-fonts/material-symbols': 0.4.27 From 878314c57c4aa6f5ab432b09ea8abe80a1c9886e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 10 Jul 2026 13:47:26 +0200 Subject: [PATCH 2/2] fix(mobile): fall back to store page when SecureStore fails in rateApp --- apps/mobile/src/lib/feedback.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/mobile/src/lib/feedback.ts b/apps/mobile/src/lib/feedback.ts index 6ae465af78..0adb00fd89 100644 --- a/apps/mobile/src/lib/feedback.ts +++ b/apps/mobile/src/lib/feedback.ts @@ -31,15 +31,15 @@ async function openSupportEmail(userId: string | undefined) { async function rateApp() { // The native review popup silently no-ops when the OS rate limit is hit, so // only use it the first time; afterwards deep-link to the store review page. - const alreadyRequested = await SecureStore.getItemAsync(REVIEW_REQUESTED_AT_KEY); - if (alreadyRequested == null && (await StoreReview.isAvailableAsync().catch(() => false))) { - await SecureStore.setItemAsync(REVIEW_REQUESTED_AT_KEY, new Date().toISOString()); - try { + try { + const alreadyRequested = await SecureStore.getItemAsync(REVIEW_REQUESTED_AT_KEY); + if (alreadyRequested == null && (await StoreReview.isAvailableAsync())) { + await SecureStore.setItemAsync(REVIEW_REQUESTED_AT_KEY, new Date().toISOString()); await StoreReview.requestReview(); return; - } catch { - // Native popup failed — fall through to the store page. } + } catch { + // Native popup path failed — fall through to the store page. } try { await Linking.openURL(STORE_REVIEW_URL);