Feat/pro preorder#357
Conversation
There was a problem hiding this comment.
Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.
There was a problem hiding this comment.
Code Review
This pull request introduces the 'Off Grid PRO' pre-order flow, including new screens for PRO details and app information, a promotional bottom sheet, and logic to trigger these prompts based on usage. Technical changes include a version bump, Android native module updates for OpenCL tuning and process execution, and the addition of environment variable transformation plugins. Feedback highlights a regression in model state management, potential path traversal vulnerabilities in the Android module, and duplicated animation indices in the settings screen. Furthermore, the PRO prompt logic should be decoupled from the share prompt state, and hardcoded strings in new components should be localized.
| const result = await DiffusionModule.generateImage(this.buildNativeParams(params, trimmedPrompt)); | ||
| // Native side releases the CoreML pipeline after generation to free | ||
| // memory, so clear TS-side state so the next request triggers a reload. | ||
| this.loadedThreads = null; | ||
| return this.buildResult(params, result); |
There was a problem hiding this comment.
The removal of this.loadedThreads = null; appears to be a regression. The original implementation cleared this state because the native side releases the CoreML pipeline after generation to conserve memory. Without resetting this on the TypeScript side, subsequent generation requests may fail to trigger a necessary reload of the model, leading to errors or stale state.
const result = await DiffusionModule.generateImage(this.buildNativeParams(params, trimmedPrompt));
// Native side releases the CoreML pipeline after generation to free
// memory, so clear TS-side state so the next request triggers a reload.
this.loadedThreads = null;
return this.buildResult(params, result);|
|
||
| val processBuilder = ProcessBuilder(command).apply { | ||
| directory(executableFile.parentFile) | ||
| directory(modelDir) |
There was a problem hiding this comment.
When setting the working directory for ProcessBuilder, verify that the directory exists to prevent the process from failing to start. Additionally, ensure the modelDir is sanitized to prevent path traversal vulnerabilities, for example by using java.io.File(modelDir).name to strip path information.
References
- Sanitize user-provided file names to prevent path traversal vulnerabilities, for example by using java.io.File(fileName).name to strip path information.
| <AppSheet visible={visible} onClose={onClose} enableDynamicSizing title="Off Grid PRO"> | ||
| <View style={styles.content}> | ||
| <Text style={styles.headline}>Loving Off Grid?</Text> | ||
| <Text style={styles.subheadline}> | ||
| Help us build what's next - and get it free for life. | ||
| </Text> | ||
|
|
||
| <View style={styles.featureList}> | ||
| {[ | ||
| 'Voice-native conversation', | ||
| 'Custom MCP servers', | ||
| 'Calendar and WhatsApp integration', | ||
| 'More, shipping monthly', | ||
| ].map(feature => ( | ||
| <View key={feature} style={styles.featureRow}> | ||
| <Icon name="check" size={14} color={styles.checkIcon.color} /> | ||
| <Text style={styles.featureText}>{feature}</Text> | ||
| </View> | ||
| ))} | ||
| </View> | ||
|
|
||
| <Text style={styles.guarantee}> | ||
| Ship in 12 weeks or full refund. No questions asked. | ||
| </Text> | ||
|
|
||
| <TouchableOpacity style={styles.ctaButton} onPress={handleCta}> | ||
| <Text style={styles.ctaText}>I am in 🔥</Text> |
There was a problem hiding this comment.
| @@ -214,15 +235,18 @@ export const SettingsScreen: React.FC = () => { | |||
|
|
|||
| {/* About */} | |||
| <AnimatedEntry index={7} staggerMs={40} trigger={focusTrigger}> | |||
There was a problem hiding this comment.
The index prop for AnimatedEntry is duplicated (both the Community section and the About section use index 7). This may cause animation timing issues or key collisions. The indices should be unique and sequential.
| <AnimatedEntry index={7} staggerMs={40} trigger={focusTrigger}> | |
| <AnimatedEntry index={8} staggerMs={40} trigger={focusTrigger}> |
| if (s.hasEngagedSharePrompt) return; | ||
| if (shouldShowSharePrompt(s.incrementTextGenerationCount())) setTimeout(() => emitSharePrompt('text'), delayMs); | ||
| checkProPromptForText(delayMs); |
There was a problem hiding this comment.
The Pro prompt check is currently gated behind hasEngagedSharePrompt. This means users who have already interacted with the share prompt will never see the Pro pre-order offer. These two prompts serve different purposes and should be evaluated independently.
const count = s.incrementTextGenerationCount();
if (!s.hasEngagedSharePrompt && shouldShowSharePrompt(count)) {
setTimeout(() => emitSharePrompt('text'), delayMs);
}
checkProPromptForText(delayMs);| if (useAppStore.getState().hasEngagedSharePrompt) return; | ||
| const count = useAppStore.getState().incrementImageGenerationCount(); | ||
| if (shouldShowSharePrompt(count)) setTimeout(() => emitSharePrompt('image'), SHARE_PROMPT_DELAY_MS); | ||
| checkProPromptForImage(SHARE_PROMPT_DELAY_MS); |
There was a problem hiding this comment.
Similar to the text generation service, the Pro prompt for images is gated behind hasEngagedSharePrompt. This prevents engaged users from seeing the Pro offer if they have already interacted with the share sheet.
const s = useAppStore.getState();
const count = s.incrementImageGenerationCount();
if (!s.hasEngagedSharePrompt && shouldShowSharePrompt(count)) {
setTimeout(() => emitSharePrompt('image'), SHARE_PROMPT_DELAY_MS);
}
checkProPromptForImage(SHARE_PROMPT_DELAY_MS);Co-Authored-By: Dishit <hanmadishit74@gmail.com>
Codecov Report❌ Patch coverage is ❌ Your patch check has failed because the patch coverage (51.85%) is below the target coverage (80.00%). You can increase the patch coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## main #357 +/- ##
==========================================
- Coverage 83.08% 82.86% -0.23%
==========================================
Files 231 235 +4
Lines 11929 12003 +74
Branches 3272 3279 +7
==========================================
+ Hits 9911 9946 +35
- Misses 1165 1200 +35
- Partials 853 857 +4
🚀 New features to boost your workflow:
|
faccba3 to
db22a63
Compare
| import { TYPOGRAPHY } from '../constants'; | ||
|
|
||
| const WEDNESDAY_URL = 'https://www.wednesday.is/?utm_source=off-grid-mobile-app'; | ||
| const WEDNESDAY_URL = 'https://mobile.wednesday.is/hire-ai-native-mobile-squad?utm_source=off-grid-mobile-app&utm_medium=made-with-love&utm_campaign=in-app'; |
There was a problem hiding this comment.
Moved to constants — WEDNESDAY_URL is now exported from src/constants/index.ts and imported from there.
| import { View, Text, TouchableOpacity } from 'react-native'; | ||
| import Icon from 'react-native-vector-icons/Feather'; | ||
| import { AppSheet } from './AppSheet'; | ||
| import { useThemedStyles } from '../theme'; |
There was a problem hiding this comment.
Fixed — merged the two ../theme imports into one.
| </Text> | ||
|
|
||
| <View style={styles.featureList}> | ||
| {[ |
There was a problem hiding this comment.
Define this in constants or somewhere else where the features make sense
There was a problem hiding this comment.
Moved to constants — PRO_AHA_FEATURES is now exported from src/constants/index.ts and used here.
| // Already in RootStack | ||
| DownloadManager: undefined; | ||
| Gallery: { conversationId?: string } | undefined; | ||
| ProDetail: undefined; |
There was a problem hiding this comment.
undefined is the correct React Navigation type for routes that take no params — it signals the route exists but accepts nothing. No change needed here.
…x duplicate import
|



feat(pro): PRO pre-order flow, about screen, and aha moment trigger
What this does
Introduces the full PRO discovery and pre-order experience, including the aha moment sheet that surfaces at the right time, a dedicated PRO detail screen, and an About screen with Wednesday branding.
Changes
PRO aha moment sheet (
ProAhaSheet.tsx)proAhaTriggeredBy- only one sheet fires per chat sessionproAhaTriggeredByis reset on each ChatScreen mount so the cycle repeats across sessionsPRO detail screen (
ProDetailScreen/index.tsx)https://offgridmobileai.coviaLinking.openURL- no backend, no email formAbout screen (
AboutScreen.tsx)Trigger logic (
src/utils/proPrompt.ts)shouldShowProAha(count)pure function - easy to test and reason aboutStore (
appStore.ts)proAhaShowCount,lastProAhaShownAthasRegisteredPro,proAhaTriggeredBy(session guard)What's deferred
hasRegisteredProcurrently set manually; will flip via deep link once implemented