Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,6 @@ repomix-output.txt

.superpowers/

@gacevicdanilo18__stroberi.jks
@gacevicdanilo18__stroberi.jks

docs/superpowers
2 changes: 1 addition & 1 deletion android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 40
versionName "1.1.0"
versionName "1.1.1"
}
signingConfigs {
debug {
Expand Down
7 changes: 4 additions & 3 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"expo": {
"name": "Stroberi",
"slug": "stroberi",
"version": "1.1.0",
"version": "1.1.1",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "stroberi",
Expand All @@ -13,7 +13,7 @@
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.stroberi",
"buildNumber": "34",
"buildNumber": "48",
"config": {
"usesNonExemptEncryption": false
}
Expand Down Expand Up @@ -77,7 +77,8 @@
}
}
],
"expo-sqlite"
"expo-sqlite",
"./plugins/withAppIntents"
],
"experiments": {
"typedRoutes": true
Expand Down
5 changes: 4 additions & 1 deletion app/(tabs)/analytics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ const AnalyticsContent = withObservables<
.get<BudgetModel>('budgets')
.query(Q.where('isActive', true))
.observe(),
budgetCategories: database.get<BudgetCategoryModel>('budget_categories').query().observe(),
budgetCategories: database
.get<BudgetCategoryModel>('budget_categories')
.query()
.observe(),
}))(({ transactions, categories, budgets, budgetCategories }: AnalyticsContentProps) => {
const { top } = useSafeAreaInsets();
const { defaultCurrency } = useDefaultCurrency();
Expand Down
23 changes: 23 additions & 0 deletions app/(tabs)/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import {
FolderOutput,
Plane,
RefreshCw,
Smartphone,
Tags,
TrendingUp,
Wallet,
} from '@tamagui/lucide-icons';
import { useRouter } from 'expo-router';
import * as React from 'react';
import { useState } from 'react';
import { Platform } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { ScrollView, Text, View, YGroup } from 'tamagui';
import { CurrencySelect } from '../../components/CurrencySelect';
Expand All @@ -20,6 +22,7 @@ import { SettingsItem } from '../../components/settings/SettingsItem';
import { ExportDataSheet } from '../../components/sheet/ExportDataSheet';
import { ImportCSVSheet } from '../../components/sheet/ImportCSVSheet';
import { ManageCategoriesSheet } from '../../components/sheet/ManageCategoriesSheet';
import { ShortcutsSetupSheet } from '../../components/sheet/ShortcutsSetupSheet';
import { ManageRecurringTransactionsSheet } from '../../components/sheet/ManageRecurringTransactionsSheet';
import {
TransactionPreviewSheet,
Expand All @@ -41,6 +44,7 @@ export default function SettingsScreen() {
null
);
const importCsvSheetRef = React.useRef<BottomSheetModal | null>(null);
const shortcutsSetupSheetRef = React.useRef<BottomSheetModal | null>(null);
const { setDefaultCurrency, defaultCurrency, isUpdatingCurrency } =
useDefaultCurrency();
const { budgetingEnabled, setBudgetingEnabled } = useBudgetingEnabled();
Expand Down Expand Up @@ -266,6 +270,24 @@ export default function SettingsScreen() {
</YGroup>
</View>

{Platform.OS === 'ios' && (
<>
<Text fontSize={'$7'} marginTop="$4" marginBottom={'$2'}>
Integrations
</Text>
<YGroup>
<SettingsItem
label={'Apple Wallet Automation'}
IconComponent={Smartphone}
rightLabel={''}
onPress={() => {
shortcutsSetupSheetRef.current?.present();
}}
/>
</YGroup>
</>
)}

<Text fontSize={'$7'} marginTop="$4" marginBottom={'$2'}>
Data
</Text>
Expand Down Expand Up @@ -336,6 +358,7 @@ export default function SettingsScreen() {
onBack={handleBackToExport}
/>
<ImportCSVSheet sheetRef={importCsvSheetRef} />
{Platform.OS === 'ios' && <ShortcutsSetupSheet sheetRef={shortcutsSetupSheetRef} />}
</>
);
}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 1 addition & 4 deletions components/home/BudgetAlertCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@ import { database } from '../../database/index';
import type { TransactionModel } from '../../database/transaction-model';
import { useBudgetingEnabled } from '../../hooks/useBudgetingEnabled';
import { useDefaultCurrency } from '../../hooks/useDefaultCurrency';
import {
calculateBudgetAlerts,
type BudgetAlertData,
} from '../../lib/budgetAlerts';
import { calculateBudgetAlerts, type BudgetAlertData } from '../../lib/budgetAlerts';
import { formatCurrency } from '../../lib/format';
import { STORAGE_KEYS } from '../../lib/storageKeys';
import { LinkButton } from '../button/LinkButton';
Expand Down
6 changes: 5 additions & 1 deletion components/sheet/ErrorSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,11 @@ export const ErrorSheet = ({
General issues
</Text>
{issueSummary.generalIssues.map((issue, index) => (
<XStack key={`${index}-${issue}`} alignItems={'flex-start'} gap={'$2'}>
<XStack
key={`${index}-${issue}`}
alignItems={'flex-start'}
gap={'$2'}
>
<Text fontSize={'$2'} color={'$red9'} mt={'$0.5'}>
</Text>
Expand Down
22 changes: 13 additions & 9 deletions components/sheet/ImportCSVSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ export const ImportCSVSheet = ({ sheetRef }: ImportCSVSheetProps) => {
current: importResult.importedCount,
message: `Imported ${importResult.importedCount} of ${validTransactions.length}; ${importResult.failed.length} skipped`,
}
: null
: null
);
}

Expand Down Expand Up @@ -443,8 +443,9 @@ export const ImportCSVSheet = ({ sheetRef }: ImportCSVSheetProps) => {
title: 'Some Rows Were Skipped',
message:
'A few rows could not be imported. Review the details below and retry those rows if needed.',
errors: importResult.failed
.map((failure) => `Row ${failure.row}: ${failure.reason}`),
errors: importResult.failed.map(
(failure) => `Row ${failure.row}: ${failure.reason}`
),
type: 'validation',
showTemplateButton: false,
showRetryButton: false,
Expand All @@ -453,8 +454,9 @@ export const ImportCSVSheet = ({ sheetRef }: ImportCSVSheetProps) => {
showError({
title: 'No Transactions Imported',
message: 'No rows could be imported. Review the issues below and try again.',
errors: importResult.failed
.map((failure) => `Row ${failure.row}: ${failure.reason}`),
errors: importResult.failed.map(
(failure) => `Row ${failure.row}: ${failure.reason}`
),
type: 'validation',
showTemplateButton: true,
showRetryButton: true,
Expand Down Expand Up @@ -541,8 +543,9 @@ export const ImportCSVSheet = ({ sheetRef }: ImportCSVSheetProps) => {
title: 'Some Rows Were Skipped',
message:
'A few rows could not be imported. Review the details below and retry those rows if needed.',
errors: importResult.failed
.map((failure) => `Row ${failure.row}: ${failure.reason}`),
errors: importResult.failed.map(
(failure) => `Row ${failure.row}: ${failure.reason}`
),
type: 'validation',
showTemplateButton: false,
showRetryButton: false,
Expand All @@ -551,8 +554,9 @@ export const ImportCSVSheet = ({ sheetRef }: ImportCSVSheetProps) => {
showError({
title: 'No Transactions Imported',
message: 'No rows could be imported. Review the issues below and try again.',
errors: importResult.failed
.map((failure) => `Row ${failure.row}: ${failure.reason}`),
errors: importResult.failed.map(
(failure) => `Row ${failure.row}: ${failure.reason}`
),
type: 'validation',
showTemplateButton: true,
showRetryButton: true,
Expand Down
178 changes: 178 additions & 0 deletions components/sheet/ShortcutsSetupSheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { BottomSheetModal, BottomSheetScrollView } from '@gorhom/bottom-sheet';
import type React from 'react';
import { useCallback, useMemo, useState } from 'react';
import { Alert, Image, Linking, NativeModules, Platform } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Spinner, Text, View } from 'tamagui';
import { Button } from '../button/Button';
import { backgroundStyle, handleIndicatorStyle } from './constants';

const renderBoldText = (text: string) => {
const parts = text.split(/(\*\*.*?\*\*)/g);
return parts.map((part, i) => {
if (part.startsWith('**') && part.endsWith('**')) {
return (
<Text key={i} fontWeight="bold" color="$gray11">
{part.slice(2, -2)}
</Text>
);
}
return part;
});
};

type ShortcutsSetupSheetProps = {
sheetRef: React.RefObject<BottomSheetModal>;
};

// Previews shown inline after the step they illustrate (0-indexed).
// IMG_3788: the finished automation (trigger + Log Transaction action).
// IMG_3789: Log Transaction action with Amount / Merchant variables mapped.
const STEP_PREVIEWS: Record<number, { source: number; caption: string }> = {
5: {
source: require('../../assets/images/ios_shortcute/IMG_3788-portrait.png'),
caption: 'Your automation should look like this.',
},
7: {
source: require('../../assets/images/ios_shortcute/IMG_3789-portrait.png'),
caption: 'Amount and Merchant should be linked to the blue variables.',
},
};

const STEPS = [
'Open the **Shortcuts** app on your iPhone',
'Go to the **Automation** tab',
'Tap **+** then choose **Wallet**',
'Pick your card(s) and tap **Next**',
'Press **Create New Shortcut**',
'Search for **"Log Transaction"** and select the Stroberi action',
"Map **Amount** to the transaction's **Amount** variable (leave it formatted — Stroberi reads the currency from it)",
'Map **Merchant** to the **Merchant** variable',
'Confirm the shortcut on top right',
];

export const ShortcutsSetupSheet = ({ sheetRef }: ShortcutsSetupSheetProps) => {
const { bottom } = useSafeAreaInsets();
const snapPoints = useMemo(() => ['90%'], []);
const [isTesting, setIsTesting] = useState(false);

const handleTestPress = useCallback(async () => {
Linking.openURL('https://www.icloud.com/shortcuts/2d1a19f1410a43bda0fc283c46c84520')
if (isTesting) return;
setIsTesting(true);
try {
const { ShortcutsModule } = NativeModules;
if (!ShortcutsModule) {
Alert.alert('Unavailable', 'Shortcuts module is not available on this device.');
return;
}
await ShortcutsModule.testLogTransaction();
Alert.alert('Success', 'Test transaction created! Check your transaction list.');
} catch {
Alert.alert('Error', 'Failed to create test transaction.');
} finally {
setIsTesting(false);
}
}, [isTesting]);
Comment on lines +59 to +76
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.

⚠️ Potential issue | 🟡 Minor

URL opens before the isTesting guard check.

Linking.openURL is called unconditionally on line 60 before the isTesting early return on line 61. If the user taps "Test It" while already testing, the URL will still open. Consider moving the URL open to after the test completes successfully, or after the guard check.

🐛 Proposed fix - open URL after guard check
   const handleTestPress = useCallback(async () => {
-    Linking.openURL('https://www.icloud.com/shortcuts/2d1a19f1410a43bda0fc283c46c84520')
     if (isTesting) return;
     setIsTesting(true);
     try {
       const { ShortcutsModule } = NativeModules;
       if (!ShortcutsModule) {
         Alert.alert('Unavailable', 'Shortcuts module is not available on this device.');
         return;
       }
       await ShortcutsModule.testLogTransaction();
       Alert.alert('Success', 'Test transaction created! Check your transaction list.');
+      Linking.openURL('https://www.icloud.com/shortcuts/2d1a19f1410a43bda0fc283c46c84520');
     } catch {
       Alert.alert('Error', 'Failed to create test transaction.');
     } finally {
       setIsTesting(false);
     }
   }, [isTesting]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleTestPress = useCallback(async () => {
Linking.openURL('https://www.icloud.com/shortcuts/2d1a19f1410a43bda0fc283c46c84520')
if (isTesting) return;
setIsTesting(true);
try {
const { ShortcutsModule } = NativeModules;
if (!ShortcutsModule) {
Alert.alert('Unavailable', 'Shortcuts module is not available on this device.');
return;
}
await ShortcutsModule.testLogTransaction();
Alert.alert('Success', 'Test transaction created! Check your transaction list.');
} catch {
Alert.alert('Error', 'Failed to create test transaction.');
} finally {
setIsTesting(false);
}
}, [isTesting]);
const handleTestPress = useCallback(async () => {
if (isTesting) return;
setIsTesting(true);
try {
const { ShortcutsModule } = NativeModules;
if (!ShortcutsModule) {
Alert.alert('Unavailable', 'Shortcuts module is not available on this device.');
return;
}
await ShortcutsModule.testLogTransaction();
Alert.alert('Success', 'Test transaction created! Check your transaction list.');
Linking.openURL('https://www.icloud.com/shortcuts/2d1a19f1410a43bda0fc283c46c84520');
} catch {
Alert.alert('Error', 'Failed to create test transaction.');
} finally {
setIsTesting(false);
}
}, [isTesting]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/sheet/ShortcutsSetupSheet.tsx` around lines 59 - 76, The
Linking.openURL call in handleTestPress is executed before the isTesting guard,
so the URL will open even when a test is already running; move the
Linking.openURL invocation to after the isTesting check (or better, after a
successful test) so it only opens when appropriate. Specifically, in
handleTestPress ensure you first check and return early on isTesting, then call
setIsTesting(true), run NativeModules.ShortcutsModule.testLogTransaction(), and
only upon success call Linking.openURL(...) and Alert.alert(...) before finally
setIsTesting(false).


const isIOS = Platform.OS === 'ios';
const iosVersion = isIOS ? Number.parseInt(Platform.Version as string, 10) : 0;
const supportsShortcuts = isIOS && iosVersion >= 16;

if (!isIOS) {
return null;
}

return (
<BottomSheetModal
ref={sheetRef}
snapPoints={snapPoints}
enableDynamicSizing={false}
enablePanDownToClose
stackBehavior="push"
animateOnMount
handleIndicatorStyle={handleIndicatorStyle}
backgroundStyle={backgroundStyle}
>
<BottomSheetScrollView
style={{ paddingHorizontal: 16 }}
contentContainerStyle={{ paddingBottom: bottom + 16 }}
>
<Text fontSize="$7" fontWeight="bold" marginBottom="$2">
Apple Pay Quick Add
</Text>
<Text fontSize="$4" color="$gray9" marginBottom="$5">
Automatically log transactions after every Apple Pay purchase using iOS
Shortcuts.
</Text>

{!supportsShortcuts && isIOS && (
<View backgroundColor="$gray3" padding="$3" borderRadius="$3" marginBottom="$4">
<Text fontSize="$4" color="$yellow10">
This feature requires iOS 16 or later. Please update your device to use
Shortcuts integration.
</Text>
</View>
)}

<Text fontSize="$5" fontWeight="600" marginBottom="$3">
Setup Instructions
</Text>

{STEPS.map((step, index) => {
return (
<View key={index} marginBottom="$3">
<View flexDirection="row" gap="$3" alignItems="center">
<View
backgroundColor="$stroberi"
borderRadius={100}
width={24}
height={24}
justifyContent="center"
alignItems="center"
flexShrink={0}
>
<Text fontSize="$3" fontWeight="bold" color="white">
{index + 1}
</Text>
</View>
<Text fontSize="$4" color="$gray11" flex={1}>
{renderBoldText(step)}
</Text>
</View>
</View>
);
})}

<View marginTop="$4">
<Text fontSize="$3" fontWeight="600" color="$gray11">
This is how it should look like in the end:
</Text>
<View flexDirection="row">
<Image
source={STEP_PREVIEWS[7].source}
resizeMode="contain"
style={{ width: '50%', height: 440, borderRadius: 12 }}
/>
<Image
source={STEP_PREVIEWS[5].source}
resizeMode="contain"
style={{ width: '50%', height: 440, borderRadius: 12 }}
/>
</View>
</View>

{supportsShortcuts && (
<View marginTop="$4">
<Button brand="primary" onPress={handleTestPress} disabled={isTesting}>
{isTesting ? <Spinner size="small" color="white" /> : 'Test It'}
</Button>
<Text fontSize="$2" color="$gray8" textAlign="center" marginTop="$2">
Creates a test transaction you can delete from your transaction list.
</Text>
</View>
)}
</BottomSheetScrollView>
</BottomSheetModal>
);
};
4 changes: 1 addition & 3 deletions database/actions/budgets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,7 @@ export const getBudgetStatus = async (
);
const transactions = await database
.get<TransactionModel>('transactions')
.query(
...buildBudgetTransactionConditions(periodStart, periodEnd, categoryIds)
)
.query(...buildBudgetTransactionConditions(periodStart, periodEnd, categoryIds))
.fetch();
const spent = sumBudgetTransactions(transactions);
let budgetLimit = budget.amount;
Expand Down
Loading