Skip to content

Feat/pro preorder#357

Merged
dishit-wednesday merged 11 commits into
mainfrom
feat/pro-preorder
May 15, 2026
Merged

Feat/pro preorder#357
dishit-wednesday merged 11 commits into
mainfrom
feat/pro-preorder

Conversation

@dishit-wednesday

@dishit-wednesday dishit-wednesday commented May 14, 2026

Copy link
Copy Markdown
Collaborator

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)

  • New bottom sheet that appears after key generation milestones to surface the PRO offering
  • Fires at message 3, then repeats at 15, 25, 35... (text and image generation counts independently)
  • Never collides with the share prompt sheet (which fires at 2, 10, 20, 30...)
  • Session guard via proAhaTriggeredBy - only one sheet fires per chat session
  • proAhaTriggeredBy is reset on each ChatScreen mount so the cycle repeats across sessions

PRO detail screen (ProDetailScreen/index.tsx)

  • Full-screen PRO feature showcase with feature list and pitch
  • Single CTA redirects to https://offgridmobileai.co via Linking.openURL - no backend, no email form
  • Email collection happens on the website

About screen (AboutScreen.tsx)

  • New screen accessible from Settings showing app info, open source notice, and Wednesday attribution
  • App logo in hero section
  • Two nav cards: Open Source (GitHub) and Built by Wednesday (with UTM link)
  • "Made with Love by Wednesday" footer pinned outside the scroll view

Trigger logic (src/utils/proPrompt.ts)

  • Count-based trigger with no cooldown or timestamps
  • shouldShowProAha(count) pure function - easy to test and reason about
  • Works for both text and image generation counts independently

Store (appStore.ts)

  • Removed cooldown-era fields: proAhaShowCount, lastProAhaShownAt
  • Kept: hasRegisteredPro, proAhaTriggeredBy (session guard)

What's deferred

  • Deep link back from website after signup (needs website-side redirect support)
  • hasRegisteredPro currently set manually; will flip via deep link once implemented

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines 174 to 175
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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
  1. Sanitize user-provided file names to prevent path traversal vulnerabilities, for example by using java.io.File(fileName).name to strip path information.

Comment on lines +24 to +50
<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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This component introduces several hardcoded user-facing strings (e.g., "Off Grid PRO", "Loving Off Grid?", etc.). To support future localization and maintain consistency, these should be moved to a localization system using the project's established patterns.

Comment thread src/screens/SettingsScreen.tsx Outdated
@@ -214,15 +235,18 @@ export const SettingsScreen: React.FC = () => {

{/* About */}
<AnimatedEntry index={7} staggerMs={40} trigger={focusTrigger}>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
<AnimatedEntry index={7} staggerMs={40} trigger={focusTrigger}>
<AnimatedEntry index={8} staggerMs={40} trigger={focusTrigger}>

Comment thread src/services/generationService.ts Outdated
Comment on lines +130 to +132
if (s.hasEngagedSharePrompt) return;
if (shouldShowSharePrompt(s.incrementTextGenerationCount())) setTimeout(() => emitSharePrompt('text'), delayMs);
checkProPromptForText(delayMs);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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);

Comment thread src/services/imageGenerationService.ts Outdated
Comment on lines +103 to +106
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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);

@codecov

codecov Bot commented May 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 51.85185% with 39 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.86%. Comparing base (6cd06f1) to head (9a65396).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
src/screens/ProDetailScreen/index.tsx 23.07% 10 Missing ⚠️
src/screens/AboutScreen.tsx 33.33% 8 Missing ⚠️
src/components/ProAhaSheet.tsx 25.00% 6 Missing ⚠️
src/utils/proPrompt.ts 73.91% 2 Missing and 4 partials ⚠️
src/screens/ChatScreen/index.tsx 58.33% 5 Missing ⚠️
src/screens/SettingsScreen.tsx 33.33% 2 Missing ⚠️
...creens/DownloadManagerScreen/useDownloadManager.ts 0.00% 1 Missing ⚠️
src/stores/appStore.ts 50.00% 1 Missing ⚠️

❌ 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

Impacted file tree graph

@@            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     
Files with missing lines Coverage Δ
src/components/MadeWithLove.tsx 100.00% <ø> (ø)
src/services/generationService.ts 97.01% <100.00%> (+0.02%) ⬆️
src/services/imageGenerationService.ts 92.25% <100.00%> (+0.69%) ⬆️
...creens/DownloadManagerScreen/useDownloadManager.ts 44.69% <0.00%> (-0.26%) ⬇️
src/stores/appStore.ts 82.75% <50.00%> (-0.78%) ⬇️
src/screens/SettingsScreen.tsx 55.31% <33.33%> (-2.46%) ⬇️
src/screens/ChatScreen/index.tsx 65.51% <58.33%> (-1.15%) ⬇️
src/components/ProAhaSheet.tsx 25.00% <25.00%> (ø)
src/utils/proPrompt.ts 73.91% <73.91%> (ø)
src/screens/AboutScreen.tsx 33.33% <33.33%> (ø)
... and 1 more

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread src/components/MadeWithLove.tsx Outdated
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';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move to constants

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove duplicate imports

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — merged the two ../theme imports into one.

Comment thread src/components/ProAhaSheet.tsx Outdated
</Text>

<View style={styles.featureList}>
{[

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Define this in constants or somewhere else where the features make sense

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to constants — PRO_AHA_FEATURES is now exported from src/constants/index.ts and used here.

Comment thread src/navigation/types.ts
// Already in RootStack
DownloadManager: undefined;
Gallery: { conversationId?: string } | undefined;
ProDetail: undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure to add types

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@sonarqubecloud

Copy link
Copy Markdown

@dishit-wednesday dishit-wednesday merged commit b18985f into main May 15, 2026
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants