diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx
index bf91398..5887916 100644
--- a/app/(tabs)/index.tsx
+++ b/app/(tabs)/index.tsx
@@ -10,11 +10,9 @@ import {
TextInput,
ActivityIndicator,
ScrollView,
- Alert,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { router } from 'expo-router';
-import { LinearGradient } from 'expo-linear-gradient';
import * as Haptics from 'expo-haptics';
import { IconSymbol } from '@/components/ui/icon-symbol';
import { SessionCard, SwipeableSessionCard, SessionCardSkeleton } from '@/components/jules/session-card';
@@ -23,6 +21,7 @@ import { useColorScheme } from '@/hooks/use-color-scheme';
import type { Session } from '@/constants/types';
import { useI18n } from '@/constants/i18n-context';
import { useApiKey } from '@/constants/api-key-context';
+import { useAlert } from '@/contexts/alert-context';
import {
useSecureStorage,
type SessionFilterPreset,
@@ -63,6 +62,7 @@ export default function SessionsScreen() {
const colorScheme = useColorScheme();
const isDark = colorScheme === 'dark';
const { t } = useI18n();
+ const { showAlert } = useAlert();
const colors = isDark ? Colors.dark : Colors.light;
const { apiKey } = useApiKey();
@@ -238,11 +238,11 @@ export default function SessionsScreen() {
}, [approvePlan, fetchSessions]);
const handleDeleteSession = useCallback((sessionName: string) => {
- Alert.alert(
+ showAlert(
t('deleteSession'),
t('deleteSessionConfirm'),
[
- { text: t('cancel'), style: 'cancel' },
+ { text: t('cancel'), style: 'cancel', onPress: () => {} },
{
text: t('delete'),
style: 'destructive',
@@ -253,7 +253,7 @@ export default function SessionsScreen() {
},
]
);
- }, [deleteSession, t]);
+ }, [deleteSession, t, showAlert]);
const renderSessionItem = useCallback(({ item }: { item: Session }) => (
handleDeleteSession(item.name)} delayLongPress={500}>
@@ -313,25 +313,13 @@ export default function SessionsScreen() {
return (
- {/* Modern Header with Gradient */}
+ {/* Modern Header */}
-
-
+
-
+
Jules Client
@@ -550,14 +538,9 @@ export default function SessionsScreen() {
accessibilityRole="button"
accessibilityHint="Create a new coding task session"
>
-
+
-
+
)}
diff --git a/app/(tabs)/settings.tsx b/app/(tabs)/settings.tsx
index f0b2f31..ae777a0 100644
--- a/app/(tabs)/settings.tsx
+++ b/app/(tabs)/settings.tsx
@@ -6,14 +6,12 @@ import {
TouchableOpacity,
StyleSheet,
ScrollView,
- Alert,
Switch,
Appearance,
Linking,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
-import { LinearGradient } from 'expo-linear-gradient';
import * as Haptics from 'expo-haptics';
import Constants from 'expo-constants';
import { IconSymbol } from '@/components/ui/icon-symbol';
@@ -21,6 +19,7 @@ import { useSecureStorage } from '@/hooks/use-secure-storage';
import { useColorScheme } from '@/hooks/use-color-scheme';
import { useI18n } from '@/constants/i18n-context';
import { useApiKey } from '@/constants/api-key-context';
+import { useAlert } from '@/contexts/alert-context';
import { Colors } from '@/constants/theme';
import { isValidExternalLink } from '@/utils/url';
@@ -93,6 +92,7 @@ export default function SettingsScreen() {
const colors = isDark ? Colors.dark : Colors.light;
const { apiKey, setApiKey: saveApiKeyToContext } = useApiKey();
+ const { showAlert } = useAlert();
const [localApiKey, setLocalApiKey] = useState(apiKey);
const [isApiKeyVisible, setIsApiKeyVisible] = useState(false);
const [manualDarkMode, setManualDarkMode] = useState(isDark);
@@ -120,10 +120,10 @@ export default function SettingsScreen() {
try {
await saveApiKeyToContext(localApiKey);
void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
- Alert.alert(t('savedSuccess'));
+ showAlert(t('savedSuccess'), undefined, undefined, 'success');
} catch {
void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
- Alert.alert(t('error'), t('savedError'));
+ showAlert(t('error'), t('savedError'), undefined, 'error');
}
};
@@ -144,31 +144,24 @@ export default function SettingsScreen() {
const openURL = async (url: string) => {
try {
if (!isValidExternalLink(url)) {
- Alert.alert(t('error'), t('unableToOpenLink') || 'Unable to open this link. Please check your device settings.');
+ showAlert(t('error'), t('unableToOpenLink') || 'Unable to open this link. Please check your device settings.', undefined, 'error');
return;
}
const supported = await Linking.canOpenURL(url);
if (supported) {
await Linking.openURL(url);
} else {
- Alert.alert(t('error'), t('unableToOpenLink') || 'Unable to open this link. Please check your device settings.');
+ showAlert(t('error'), t('unableToOpenLink') || 'Unable to open this link. Please check your device settings.', undefined, 'error');
}
} catch (error) {
- Alert.alert(t('error'), t('unableToOpenLink') || 'Unable to open this link. Please try again later.');
+ showAlert(t('error'), t('unableToOpenLink') || 'Unable to open this link. Please try again later.', undefined, 'error');
}
};
return (
- {/* Modern Header with Gradient */}
+ {/* Modern Header */}
-
{t('settings')}
@@ -212,15 +205,10 @@ export default function SettingsScreen() {
onPress={handleSave}
activeOpacity={0.9}
>
-
+
{t('save')}
-
+
{/* テーマ切り替え */}
diff --git a/app/_layout.tsx b/app/_layout.tsx
index 6700c1c..b3c2237 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -5,6 +5,7 @@ import 'react-native-reanimated';
import { ApiKeyProvider } from '@/constants/api-key-context';
import { I18nProvider } from '@/constants/i18n-context';
+import { AlertProvider } from '@/contexts/alert-context';
import { useColorScheme } from '@/hooks/use-color-scheme';
export default function RootLayout() {
@@ -13,15 +14,17 @@ export default function RootLayout() {
return (
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
);
diff --git a/app/create-session.tsx b/app/create-session.tsx
index a21c5db..dd2e955 100644
--- a/app/create-session.tsx
+++ b/app/create-session.tsx
@@ -4,7 +4,6 @@ import {
Text,
TouchableOpacity,
ScrollView,
- Alert,
KeyboardAvoidingView,
Platform,
} from 'react-native';
@@ -19,6 +18,7 @@ import { useSecureStorage } from '@/hooks/use-secure-storage';
import { useSourcesCache } from '@/hooks/use-sources-cache';
import type { Source } from '@/constants/types';
import { Colors } from '@/constants/theme';
+import { useAlert } from '@/contexts/alert-context';
import { FormSkeleton } from '@/components/jules/create-session/form-skeleton';
import { SourceSelector } from '@/components/jules/create-session/source-selector';
@@ -38,6 +38,7 @@ export default function CreateSessionScreen() {
const { apiKey } = useApiKey();
const { saveRecentRepo, getRecentRepos } = useSecureStorage();
const { getCachedSources, saveCachedSources } = useSourcesCache();
+ const { showAlert } = useAlert();
const [selectedSource, setSelectedSource] = useState('');
const [prompt, setPrompt] = useState('');
const [requirePlanApproval, setRequirePlanApproval] = useState(false); // false = Start/Run, true = Review
@@ -174,12 +175,12 @@ export default function CreateSessionScreen() {
// Create session and save to recent repos
const handleCreate = useCallback(async () => {
if (!selectedSource || !prompt.trim()) {
- Alert.alert(t('error'), t('inputError'));
+ showAlert(t('error'), t('inputError'), undefined, 'error');
return;
}
if (prompt.length > 50000) {
- Alert.alert(t('error'), t('promptTooLong'));
+ showAlert(t('error'), t('promptTooLong'), undefined, 'error');
return;
}
@@ -191,14 +192,14 @@ export default function CreateSessionScreen() {
if (session && source) {
// Save to recent repos
await saveRecentRepo(source);
- Alert.alert(t('createSuccess'), '', [
+ showAlert(t('createSuccess'), undefined, [
{
text: 'OK',
onPress: () => router.back(),
},
- ]);
+ ], 'success');
}
- }, [selectedSource, prompt, requirePlanApproval, sourcesMap, createSession, saveRecentRepo, t]);
+ }, [selectedSource, prompt, requirePlanApproval, sourcesMap, createSession, saveRecentRepo, t, showAlert]);
return (
<>
diff --git a/app/session/id.tsx b/app/session/id.tsx
index 8634bb6..19a55eb 100644
--- a/app/session/id.tsx
+++ b/app/session/id.tsx
@@ -11,14 +11,12 @@ import {
Platform,
Animated,
Linking,
- Alert,
ActionSheetIOS,
} from 'react-native';
import { useLocalSearchParams, Stack } from 'expo-router';
import { useHeaderHeight } from '@react-navigation/elements';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import * as Haptics from 'expo-haptics';
-import { LinearGradient } from 'expo-linear-gradient';
import { IconSymbol } from '@/components/ui/icon-symbol';
import { ActivityItem, ActivityItemSkeleton } from '@/components/jules';
import { useJulesApi } from '@/hooks/use-jules-api';
@@ -27,6 +25,7 @@ import { shareSession } from '@/hooks/use-export-session';
import type { Activity, Session } from '@/constants/types';
import { useI18n } from '@/constants/i18n-context';
import { useApiKey } from '@/constants/api-key-context';
+import { useAlert } from '@/contexts/alert-context';
import { SessionHeaderRight } from '@/components/jules/session-header-right';
import { ErrorBanner } from '@/components/jules/error-banner';
import { ApprovalBanner } from '@/components/jules/approval-banner';
@@ -239,7 +238,7 @@ export default function SessionDetailScreen() {
// Export session handler
const handleExportSession = useCallback(async (format: 'markdown' | 'json') => {
if (!currentSession || activities.length === 0) {
- Alert.alert(t('error'), t('noSessionDataToExport'));
+ showAlert(t('error'), t('noSessionDataToExport'), undefined, 'error');
return;
}
@@ -250,12 +249,12 @@ export default function SessionDetailScreen() {
} catch (err) {
const errorMessage = err instanceof Error ? err.message : t('exportFailed');
if (errorMessage.includes('not available')) {
- Alert.alert(t('error'), t('sharingNotAvailable'));
+ showAlert(t('error'), t('sharingNotAvailable'), undefined, 'error');
} else {
- Alert.alert(t('error'), errorMessage);
+ showAlert(t('error'), errorMessage, undefined, 'error');
}
}
- }, [currentSession, activities, t]);
+ }, [currentSession, activities, t, showAlert]);
// Show export menu
const showExportMenu = useCallback(() => {
@@ -277,17 +276,17 @@ export default function SessionDetailScreen() {
);
} else {
// Android - show simple alert
- Alert.alert(
+ showAlert(
t('exportSession'),
t('chooseExportFormat'),
[
- { text: t('cancel'), style: 'cancel' },
+ { text: t('cancel'), style: 'cancel', onPress: () => {} },
{ text: t('exportAsMarkdown'), onPress: () => void handleExportSession('markdown') },
{ text: t('exportAsJSON'), onPress: () => void handleExportSession('json') },
]
);
}
- }, [t, handleExportSession]);
+ }, [t, handleExportSession, showAlert]);
return (
@@ -370,11 +369,7 @@ export default function SessionDetailScreen() {
if (url) void Linking.openURL(url);
}}
>
-
+
Pull Request Created!
@@ -385,7 +380,7 @@ export default function SessionDetailScreen() {
)}
-
+
) : null
}
diff --git a/app/statistics.tsx b/app/statistics.tsx
index d563b16..182028e 100644
--- a/app/statistics.tsx
+++ b/app/statistics.tsx
@@ -2,7 +2,6 @@ import React, { useMemo } from 'react';
import { View, Text, StyleSheet, ScrollView } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { Stack } from 'expo-router';
-import { LinearGradient } from 'expo-linear-gradient';
import { IconSymbol } from '@/components/ui/icon-symbol';
import { useColorScheme } from '@/hooks/use-color-scheme';
import { useI18n } from '@/constants/i18n-context';
@@ -56,66 +55,46 @@ export default function StatisticsScreen() {
{/* Total Sessions */}
-
+
{t('totalSessions')}
{stats.total}
-
+
{/* Active Sessions */}
-
+
{t('activeSessions')}
{stats.active}
-
+
{/* Completed Sessions */}
-
+
{t('completedSessions')}
{stats.completed}
-
+
{/* Failed Sessions */}
-
+
{t('failedSessions')}
{stats.failed}
-
+
{/* Info Section */}
diff --git a/bun.lock b/bun.lock
index 9232ab1..6b5960b 100644
--- a/bun.lock
+++ b/bun.lock
@@ -17,7 +17,6 @@
"expo-haptics": "~15.0.8",
"expo-image": "~3.0.11",
"expo-image-picker": "~17.0.10",
- "expo-linear-gradient": "^15.0.8",
"expo-linking": "~8.0.11",
"expo-router": "~6.0.21",
"expo-secure-store": "~15.0.8",
@@ -937,8 +936,6 @@
"expo-keep-awake": ["expo-keep-awake@15.0.8", "", { "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-YK9M1VrnoH1vLJiQzChZgzDvVimVoriibiDIFLbQMpjYBnvyfUeHJcin/Gx1a+XgupNXy92EQJLgI/9ZuXajYQ=="],
- "expo-linear-gradient": ["expo-linear-gradient@15.0.8", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-V2d8Wjn0VzhPHO+rrSBtcl+Fo+jUUccdlmQ6OoL9/XQB7Qk3d9lYrqKDJyccwDxmQT10JdST3Tmf2K52NLc3kw=="],
-
"expo-linking": ["expo-linking@8.0.11", "", { "dependencies": { "expo-constants": "~18.0.12", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-+VSaNL5om3kOp/SSKO5qe6cFgfSIWnnQDSbA7XLs3ECkYzXRquk5unxNS3pg7eK5kNUmQ4kgLI7MhTggAEUBLA=="],
"expo-modules-autolinking": ["expo-modules-autolinking@3.0.23", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "commander": "^7.2.0", "require-from-string": "^2.0.2", "resolve-from": "^5.0.0" }, "bin": { "expo-modules-autolinking": "bin/expo-modules-autolinking.js" } }, "sha512-YZnaE0G+52xftjH5nsIRaWsoVBY38SQCECclpdgLisdbRY/6Mzo7ndokjauOv3mpFmzMZACHyJNu1YSAffQwTg=="],
diff --git a/components/jules/activity-item.tsx b/components/jules/activity-item.tsx
index d785db6..bb64141 100644
--- a/components/jules/activity-item.tsx
+++ b/components/jules/activity-item.tsx
@@ -1,7 +1,6 @@
import React, { useState, useEffect, useRef } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Animated, Image } from 'react-native';
import Markdown from 'react-native-markdown-display';
-import { LinearGradient } from 'expo-linear-gradient';
import * as Haptics from 'expo-haptics';
import { IconSymbol } from '@/components/ui/icon-symbol';
import { useColorScheme } from '@/hooks/use-color-scheme';
@@ -245,14 +244,9 @@ function AgentMessageActivity({ activity, isDark, colors, formatTime, getMarkdow
-
+
-
+
@@ -272,12 +266,7 @@ function UserMessageActivity({ activity, isDark, colors, formatTime }: { activit
return (
-
+
You
{formatTime(activity.createTime)}
@@ -285,7 +274,7 @@ function UserMessageActivity({ activity, isDark, colors, formatTime }: { activit
{activity.userMessaged!.userMessage}
-
+
@@ -302,12 +291,9 @@ function PlanGeneratedActivity({ activity, isDark, colors }: { activity: Activit
-
+
-
+
Plan Generated
{plan.steps?.map((step, index) => (
@@ -331,21 +317,11 @@ function PlanApprovalRequestedActivity({ activity, isDark, colors, onApprovePlan
const planId = activity.planApprovalRequested!.planId;
return (
-
-
+
-
+
-
+
Approval Required
@@ -360,15 +336,10 @@ function PlanApprovalRequestedActivity({ activity, isDark, colors, onApprovePlan
}}
activeOpacity={0.9}
>
-
+
Approve Plan
-
+
)}
diff --git a/components/jules/create-session/submit-button.tsx b/components/jules/create-session/submit-button.tsx
index 86ce8ee..fe581e4 100644
--- a/components/jules/create-session/submit-button.tsx
+++ b/components/jules/create-session/submit-button.tsx
@@ -1,6 +1,5 @@
import React from 'react';
import { View, Text, TouchableOpacity, ActivityIndicator } from 'react-native';
-import { LinearGradient } from 'expo-linear-gradient';
import * as Haptics from 'expo-haptics';
import { IconSymbol } from '@/components/ui/icon-symbol';
import { styles } from './styles';
@@ -45,19 +44,14 @@ export function SubmitButton({
) : (
-
+
{isLoading ? (
) : (
)}
{buttonLabel}
-
+
)}
);
diff --git a/components/jules/feedback-banner.tsx b/components/jules/feedback-banner.tsx
index 0e0e539..32b1bdb 100644
--- a/components/jules/feedback-banner.tsx
+++ b/components/jules/feedback-banner.tsx
@@ -1,6 +1,5 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
-import { LinearGradient } from 'expo-linear-gradient';
import { IconSymbol } from '@/components/ui/icon-symbol';
interface FeedbackBannerProps {
@@ -13,13 +12,15 @@ export function FeedbackBanner({ sessionState, isDark, t }: FeedbackBannerProps)
if (sessionState !== 'AWAITING_USER_FEEDBACK') return null;
return (
-
-
+
Jules is waiting for your response
diff --git a/components/jules/pr-card.tsx b/components/jules/pr-card.tsx
index 3814e80..c17ec9c 100644
--- a/components/jules/pr-card.tsx
+++ b/components/jules/pr-card.tsx
@@ -1,7 +1,8 @@
import React from 'react';
-import { View, Text, TouchableOpacity, StyleSheet, Linking, Alert } from 'react-native';
+import { View, Text, TouchableOpacity, StyleSheet, Linking } from 'react-native';
import { IconSymbol } from '@/components/ui/icon-symbol';
import { isValidExternalLink } from '@/utils/url';
+import { useAlert } from '@/contexts/alert-context';
import type { PullRequest } from '@/constants/types';
@@ -12,6 +13,8 @@ interface PrCardProps {
}
export function PrCard({ submittedPr, isDark, t }: PrCardProps) {
+ const { showAlert } = useAlert();
+
if (!submittedPr) return null;
const isObject = typeof submittedPr === 'object';
@@ -36,7 +39,7 @@ export function PrCard({ submittedPr, isDark, t }: PrCardProps) {
if (url && isValidExternalLink(url)) {
void Linking.openURL(url);
} else {
- Alert.alert(t('error'), t('unableToOpenLink'));
+ showAlert(t('error'), t('unableToOpenLink'), undefined, 'error');
}
}}
activeOpacity={0.7}
diff --git a/components/jules/session-card.tsx b/components/jules/session-card.tsx
index 3d3a3ed..c93f6d4 100644
--- a/components/jules/session-card.tsx
+++ b/components/jules/session-card.tsx
@@ -1,6 +1,5 @@
import React, { useEffect, useRef } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, Animated, ActivityIndicator } from 'react-native';
-import { LinearGradient } from 'expo-linear-gradient';
import * as Haptics from 'expo-haptics';
import { Swipeable } from 'react-native-gesture-handler';
import { IconSymbol } from '@/components/ui/icon-symbol';
@@ -214,16 +213,10 @@ export const SessionCard = React.memo(function SessionCard({ session, onPress, o
/>
)}
- {/* Gradient accent for active sessions */}
+ {/* Flat accent for active sessions */}
{isActiveState && (
-
)}
diff --git a/components/jules/session-header-right.tsx b/components/jules/session-header-right.tsx
index 144b3f5..384bfe2 100644
--- a/components/jules/session-header-right.tsx
+++ b/components/jules/session-header-right.tsx
@@ -1,7 +1,8 @@
import React from 'react';
-import { View, Text, TouchableOpacity, StyleSheet, Linking, Alert } from 'react-native';
+import { View, Text, TouchableOpacity, StyleSheet, Linking } from 'react-native';
import { IconSymbol } from '@/components/ui/icon-symbol';
import { isValidExternalLink } from '@/utils/url';
+import { useAlert } from '@/contexts/alert-context';
export function getSessionStateText(state: string | null, t: (key: string) => string): string {
if (!state) return '';
@@ -29,6 +30,8 @@ interface SessionHeaderRightProps {
}
export function SessionHeaderRight({ sessionState, sessionUrl, isDark, t, showExportMenu, loadActivities }: SessionHeaderRightProps) {
+ const { showAlert } = useAlert();
+
return (
{sessionState && (
@@ -55,7 +58,7 @@ export function SessionHeaderRight({ sessionState, sessionUrl, isDark, t, showEx
if (isValidExternalLink(sessionUrl)) {
void Linking.openURL(sessionUrl);
} else {
- Alert.alert(t('error'), t('unableToOpenLink'));
+ showAlert(t('error'), t('unableToOpenLink'), undefined, 'error');
}
}}
accessibilityLabel="Open in Web"
diff --git a/components/ui/custom-alert.tsx b/components/ui/custom-alert.tsx
new file mode 100644
index 0000000..1cc00ff
--- /dev/null
+++ b/components/ui/custom-alert.tsx
@@ -0,0 +1,262 @@
+import React, { useEffect } from 'react';
+import { View, Text, StyleSheet, TouchableOpacity, Modal, Animated } from 'react-native';
+import { Colors } from '@/constants/theme';
+import { IconSymbol } from '@/components/ui/icon-symbol';
+
+export type AlertType = 'success' | 'error' | 'info';
+
+interface CustomAlertProps {
+ visible: boolean;
+ title: string;
+ message?: string;
+ type?: AlertType;
+ buttons?: {
+ text: string;
+ onPress: () => void;
+ style?: 'default' | 'cancel' | 'destructive';
+ }[];
+ onClose: () => void;
+ isDark: boolean;
+}
+
+export function CustomAlert({
+ visible,
+ title,
+ message,
+ type = 'info',
+ buttons,
+ onClose,
+ isDark,
+}: CustomAlertProps) {
+ const scaleValue = React.useRef(new Animated.Value(0.9)).current;
+ const opacityValue = React.useRef(new Animated.Value(0)).current;
+ const [modalVisible, setModalVisible] = React.useState(visible);
+
+ useEffect(() => {
+ if (visible) {
+ setModalVisible(true);
+ Animated.parallel([
+ Animated.spring(scaleValue, {
+ toValue: 1,
+ useNativeDriver: true,
+ tension: 65,
+ friction: 7,
+ }),
+ Animated.timing(opacityValue, {
+ toValue: 1,
+ duration: 200,
+ useNativeDriver: true,
+ }),
+ ]).start();
+ } else {
+ Animated.parallel([
+ Animated.timing(scaleValue, {
+ toValue: 0.9,
+ duration: 150,
+ useNativeDriver: true,
+ }),
+ Animated.timing(opacityValue, {
+ toValue: 0,
+ duration: 150,
+ useNativeDriver: true,
+ }),
+ ]).start(() => {
+ setModalVisible(false);
+ });
+ }
+ }, [visible, scaleValue, opacityValue]);
+
+ if (!modalVisible) return null;
+
+ const getIcon = () => {
+ switch (type) {
+ case 'success':
+ return ;
+ case 'error':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ const defaultButtons = buttons || [
+ { text: 'OK', onPress: onClose, style: 'default' }
+ ];
+
+ return (
+
+
+
+
+
+ {getIcon()}
+
+
+
+ {title}
+
+
+ {message ? (
+
+ {message}
+
+ ) : null}
+
+ 2 && styles.buttonContainerVertical]}>
+ {defaultButtons.map((btn, index) => {
+ const isDestructive = btn.style === 'destructive';
+ const isCancel = btn.style === 'cancel';
+
+ return (
+ 2) && styles.buttonFull
+ ]}
+ onPress={() => {
+ btn.onPress();
+ if (btn.onPress !== onClose) {
+ onClose();
+ }
+ }}
+ >
+
+ {btn.text}
+
+
+ );
+ })}
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ overlay: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ backdrop: {
+ ...StyleSheet.absoluteFillObject,
+ backgroundColor: 'rgba(0, 0, 0, 0.4)',
+ },
+ alertBox: {
+ width: '85%',
+ maxWidth: 340,
+ borderRadius: 16,
+ padding: 24,
+ alignItems: 'center',
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.15,
+ shadowRadius: 12,
+ elevation: 8,
+ },
+ alertBoxLight: {
+ backgroundColor: '#ffffff',
+ },
+ alertBoxDark: {
+ backgroundColor: '#1e293b',
+ borderWidth: 1,
+ borderColor: '#334155',
+ },
+ iconContainer: {
+ marginBottom: 16,
+ },
+ title: {
+ fontSize: 18,
+ fontWeight: '700',
+ marginBottom: 8,
+ textAlign: 'center',
+ },
+ textLight: {
+ color: '#0f172a',
+ },
+ textDark: {
+ color: '#f8fafc',
+ },
+ message: {
+ fontSize: 15,
+ textAlign: 'center',
+ marginBottom: 24,
+ lineHeight: 22,
+ },
+ messageLight: {
+ color: '#64748b',
+ },
+ messageDark: {
+ color: '#94a3b8',
+ },
+ buttonContainer: {
+ flexDirection: 'row',
+ width: '100%',
+ gap: 12,
+ },
+ buttonContainerVertical: {
+ flexDirection: 'column',
+ gap: 8,
+ },
+ button: {
+ backgroundColor: Colors.light.primary,
+ paddingVertical: 14,
+ borderRadius: 10,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ buttonHalf: {
+ flex: 1,
+ },
+ buttonFull: {
+ width: '100%',
+ },
+ buttonDestructive: {
+ backgroundColor: Colors.light.error,
+ },
+ buttonCancelLight: {
+ backgroundColor: '#f1f5f9',
+ },
+ buttonCancelDark: {
+ backgroundColor: '#334155',
+ },
+ buttonText: {
+ color: '#ffffff',
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ buttonTextDestructive: {
+ color: '#ffffff',
+ },
+ buttonTextCancelLight: {
+ color: '#475569',
+ },
+ buttonTextCancelDark: {
+ color: '#cbd5e1',
+ },
+});
diff --git a/contexts/alert-context.tsx b/contexts/alert-context.tsx
new file mode 100644
index 0000000..546c829
--- /dev/null
+++ b/contexts/alert-context.tsx
@@ -0,0 +1,37 @@
+import React, { createContext, useContext, ReactNode } from 'react';
+import { useColorScheme } from 'react-native';
+import { CustomAlert } from '@/components/ui/custom-alert';
+import { useCustomAlert, type AlertState } from '@/hooks/use-custom-alert';
+import type { AlertType } from '@/components/ui/custom-alert';
+
+interface AlertContextType {
+ showAlert: (
+ title: string,
+ message?: string,
+ buttons?: AlertState['buttons'],
+ type?: AlertType
+ ) => void;
+ hideAlert: () => void;
+}
+
+const AlertContext = createContext(undefined);
+
+export function AlertProvider({ children }: { children: ReactNode }) {
+ const { alertState, showAlert, hideAlert } = useCustomAlert();
+ const isDark = useColorScheme() === 'dark';
+
+ return (
+
+ {children}
+
+
+ );
+}
+
+export function useAlert() {
+ const context = useContext(AlertContext);
+ if (!context) {
+ throw new Error('useAlert must be used within an AlertProvider');
+ }
+ return context;
+}
diff --git a/hooks/use-custom-alert.ts b/hooks/use-custom-alert.ts
new file mode 100644
index 0000000..3af0f9d
--- /dev/null
+++ b/hooks/use-custom-alert.ts
@@ -0,0 +1,49 @@
+import { useState, useCallback } from 'react';
+import type { AlertType } from '@/components/ui/custom-alert';
+
+export interface AlertState {
+ visible: boolean;
+ title: string;
+ message?: string;
+ type?: AlertType;
+ buttons?: {
+ text: string;
+ onPress: () => void;
+ style?: 'default' | 'cancel' | 'destructive';
+ }[];
+}
+
+export function useCustomAlert() {
+ const [alertState, setAlertState] = useState({
+ visible: false,
+ title: '',
+ });
+
+ const showAlert = useCallback(
+ (
+ title: string,
+ message?: string,
+ buttons?: AlertState['buttons'],
+ type: AlertType = 'info'
+ ) => {
+ setAlertState({
+ visible: true,
+ title,
+ message,
+ type,
+ buttons,
+ });
+ },
+ []
+ );
+
+ const hideAlert = useCallback(() => {
+ setAlertState((prev) => ({ ...prev, visible: false }));
+ }, []);
+
+ return {
+ alertState,
+ showAlert,
+ hideAlert,
+ };
+}
diff --git a/output.log b/output.log
new file mode 100644
index 0000000..4565272
--- /dev/null
+++ b/output.log
@@ -0,0 +1,15 @@
+Starting project at /app
+React Compiler enabled
+Starting Metro Bundler
+The following packages should be updated for best compatibility with the installed expo version:
+ expo@54.0.30 - expected version: ~54.0.34
+ expo-constants@18.0.12 - expected version: ~18.0.13
+ expo-file-system@18.0.12 - expected version: ~19.0.22
+ expo-font@14.0.10 - expected version: ~14.0.11
+ expo-image-picker@17.0.10 - expected version: ~17.0.11
+ expo-linking@8.0.11 - expected version: ~8.0.12
+ expo-router@6.0.21 - expected version: ~6.0.23
+ expo-web-browser@15.0.10 - expected version: ~15.0.11
+Your project may not work correctly until you install the expected versions of the packages.
+Waiting on http://localhost:8081
+Logs for your project will appear below.
diff --git a/package.json b/package.json
index 7280aae..0a4942f 100644
--- a/package.json
+++ b/package.json
@@ -27,7 +27,6 @@
"expo-haptics": "~15.0.8",
"expo-image": "~3.0.11",
"expo-image-picker": "~17.0.10",
- "expo-linear-gradient": "^15.0.8",
"expo-linking": "~8.0.11",
"expo-router": "~6.0.21",
"expo-secure-store": "~15.0.8",
diff --git a/serve_output.log b/serve_output.log
new file mode 100644
index 0000000..d194c81
Binary files /dev/null and b/serve_output.log differ