From ec71516f65cc3fc8b544740b1b83eccb228ce265 Mon Sep 17 00:00:00 2001 From: Kwaku Ansah Date: Mon, 7 Jul 2025 03:48:50 +0000 Subject: [PATCH 01/44] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fd14a5b..22102d9 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@
-![Dooit Logo](./assets/images/app-icon-all.png){width=200} +![Dooit Logo](./assets/images/app-icon-all.png){width=100}
From 80736708fdc0f63bb2d93d116dc3f52f6e00aafe Mon Sep 17 00:00:00 2001 From: Kwaku Ansah Date: Mon, 7 Jul 2025 03:51:09 +0000 Subject: [PATCH 02/44] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 22102d9..1988b5c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@
-![Dooit Logo](./assets/images/app-icon-all.png){width=100} +Dooit Logo
From 1c42250aa4b129e58c721948a06d209532d5bb94 Mon Sep 17 00:00:00 2001 From: Kwaku Ansah Date: Mon, 7 Jul 2025 03:56:11 +0000 Subject: [PATCH 03/44] Update README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 1988b5c..830563e 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,6 @@ A powerful and user-friendly to-do list application built with React Native and - Clean and modern design - Responsive layout - Intuitive navigation - - Dark/light theme support ## πŸ›  Tech Stack From 79c917af07fa6349fe90290a5adca38eb5360470 Mon Sep 17 00:00:00 2001 From: neweracy Date: Mon, 7 Jul 2025 06:18:14 +0000 Subject: [PATCH 04/44] feat: autthentication --- app/models/AuthenticationStore.ts | 2 +- app/navigators/AppNavigator.tsx | 15 ++++-- app/navigators/types.ts | 12 +++++ app/screens/ChooseAuthScreen.tsx | 87 +++++++++++++++++++++++++++++++ app/screens/LoginScreen.tsx | 21 +++++++- app/screens/WelcomeScreen.tsx | 12 +++-- app/screens/index.ts | 1 + 7 files changed, 139 insertions(+), 11 deletions(-) create mode 100644 app/navigators/types.ts create mode 100644 app/screens/ChooseAuthScreen.tsx diff --git a/app/models/AuthenticationStore.ts b/app/models/AuthenticationStore.ts index 3d35ae5..e9790e2 100644 --- a/app/models/AuthenticationStore.ts +++ b/app/models/AuthenticationStore.ts @@ -19,7 +19,7 @@ export const AuthenticationStoreModel = types }, })) .actions((store) => ({ - setAuthToken(value?: string) { + setAuthToken(value?: string, authPassword?: string) { store.authToken = value }, setAuthEmail(value: string) { diff --git a/app/navigators/AppNavigator.tsx b/app/navigators/AppNavigator.tsx index b17ccae..e0ca639 100644 --- a/app/navigators/AppNavigator.tsx +++ b/app/navigators/AppNavigator.tsx @@ -13,7 +13,7 @@ import { useStores } from "../models" import { DemoNavigator, DemoTabParamList } from "./DemoNavigator" import { navigationRef, useBackButtonHandler } from "./navigationUtilities" import { useAppTheme, useThemeProvider } from "@/utils/useAppTheme" -import { ComponentProps } from "react" +import { ComponentProps, useEffect } from "react" /** * This type allows TypeScript to know what routes are defined in this navigator @@ -33,7 +33,8 @@ export type AppStackParamList = { Login: undefined Demo: NavigatorScreenParams // πŸ”₯ Your screens go here - // IGNITE_GENERATOR_ANCHOR_APP_STACK_PARAM_LIST + ChooseAuth: undefined + // IGNITE_GENERATOR_ANCHOR_APP_STACK_PARAM_LIST } /** @@ -68,22 +69,23 @@ const AppStack = observer(function AppStack() { backgroundColor: colors.background, }, }} - initialRouteName={isAuthenticated ? "Welcome" : "Login"} + initialRouteName={isAuthenticated ? "Demo" : "Welcome"} > {isAuthenticated ? ( <> - ) : ( <> + + )} {/** πŸ”₯ Your screens go here */} - {/* IGNITE_GENERATOR_ANCHOR_APP_STACK_SCREENS */} + {/* IGNITE_GENERATOR_ANCHOR_APP_STACK_SCREENS */} ) }) @@ -94,6 +96,9 @@ export interface NavigationProps export const AppNavigator = observer(function AppNavigator(props: NavigationProps) { const { themeScheme, navigationTheme, setThemeContextOverride, ThemeProvider } = useThemeProvider() + useEffect(() => { + setThemeContextOverride("light") + }, [setThemeContextOverride]) const exitRoutes = ["welcome","Demo"] // Add any other routes that should exit the app here // This hook will handle the back button on Android and exit the app if the user is on an exit route diff --git a/app/navigators/types.ts b/app/navigators/types.ts new file mode 100644 index 0000000..eb48706 --- /dev/null +++ b/app/navigators/types.ts @@ -0,0 +1,12 @@ +import { NativeStackScreenProps } from '@react-navigation/native-stack'; + +export type RootStackParamList = { + Welcome: undefined; + ChooseAuth: undefined; + Login: undefined; + Demo: undefined; + // Add other screens here as needed +}; + +export type AppStackScreenProps = + NativeStackScreenProps; diff --git a/app/screens/ChooseAuthScreen.tsx b/app/screens/ChooseAuthScreen.tsx new file mode 100644 index 0000000..332fd29 --- /dev/null +++ b/app/screens/ChooseAuthScreen.tsx @@ -0,0 +1,87 @@ +import { FC } from "react"; +import { observer } from "mobx-react-lite"; +import { ViewStyle, View, Image } from "react-native"; +import { AppStackScreenProps } from "@/navigators/types"; +import { AutoImage, Button, Icon, Screen, Text } from "@/components"; +import { useNavigation } from "@react-navigation/native"; +import { useStores } from "@/models"; +import { useHeader } from "@/utils/useHeader"; +import { ThemedStyle } from "@/theme"; +import { useAppTheme } from "@/utils/useAppTheme"; + +interface ChooseAuthScreenProps extends AppStackScreenProps<"ChooseAuth"> {} + +const logo = require("../../assets/images/app-icon-android-adaptive-foreground.png"); + +export const ChooseAuthScreen: FC> = observer( + function ChooseAuthScreen(_props) { + // Pull in one of our MST stores + const { authenticationStore } = useStores(); + const navigation = useNavigation(); + + useHeader( + { + leftIcon: "back", + title: "Welcome to Dooit", + onLeftPress: () => navigation.goBack(), + }, + [navigation] + ); + + const { + themed, + theme: { colors }, + } = useAppTheme(); + + // Pull in navigation via hook + // const navigation = useNavigation() + return ( + + + + + ) : undefined + } + RightComponent={ + + + } /> ) @@ -318,69 +442,136 @@ const $listContentContainer: ThemedStyle = ({ spacing }) => ({ const $heading: ThemedStyle = ({ spacing }) => ({ marginBottom: spacing.md, + alignItems: "center", +}) + +const $subtitle: ThemedStyle = ({ colors, spacing }) => ({ + color: colors.textDim, + marginTop: spacing.xs, +}) + +const $filterContainer: ThemedStyle = ({ spacing }) => ({ + marginBottom: spacing.md, +}) + +const $filterLabel: ThemedStyle = ({ colors, spacing }) => ({ + color: colors.textDim, + marginBottom: spacing.xs, + fontWeight: "600", +}) + +const $filterButtonRow: ThemedStyle = ({ spacing }) => ({ + + flexDirection: "row", + flexWrap: "wrap", + gap: spacing.xs, +}) + +const $filterButton: ThemedStyle = ({ colors, spacing }) => ({ + backgroundColor: colors.palette.neutral200, + borderColor: colors.palette.neutral300, + borderRadius: 16, + paddingHorizontal: spacing.sm, + paddingVertical: spacing.xs, + minHeight: 32, +}) + +const $periodFilterButton: ThemedStyle = ({ colors, spacing }) => ({ + backgroundColor: colors.palette.neutral200, + borderColor: colors.palette.neutral300, + borderRadius: 16, + paddingHorizontal: spacing.sm, + paddingVertical: spacing.xs, + minHeight: 32, + flex: 1, + maxWidth: "27%", +}) + +const $activeFilterButton: ThemedStyle = ({ colors }) => ({ + backgroundColor: colors.palette.primary100, + borderColor: colors.palette.primary300, +}) + +const $filterButtonText: ThemedStyle = ({ colors }) => ({ + color: colors.textDim, + fontSize: 12, + fontWeight: "500", +}) + +const $activeFilterButtonText: ThemedStyle = ({ colors }) => ({ + color: colors.palette.primary600, + fontWeight: "600", }) const $item: ThemedStyle = ({ colors, spacing }) => ({ padding: spacing.md, marginTop: spacing.md, - minHeight: 120, + minHeight: 100, backgroundColor: colors.palette.neutral100, }) -const $itemThumbnail: ThemedStyle = ({ spacing }) => ({ - marginTop: spacing.sm, - borderRadius: 50, - alignSelf: "flex-start", +const $completedItem: ThemedStyle = ({ colors }) => ({ + backgroundColor: colors.palette.neutral100, + opacity: 0.8, }) -const $toggle: ThemedStyle = ({ spacing }) => ({ - marginTop: spacing.md, +const $completedText: ThemedStyle = ({ colors }) => ({ + textDecorationLine: "line-through", + color: colors.textDim, }) -const $labelStyle: TextStyle = { - textAlign: "left", -} +const $description: ThemedStyle = ({ colors, spacing }) => ({ + color: colors.textDim, + marginTop: spacing.xs, + fontStyle: "italic", +}) const $iconContainer: ThemedStyle = ({ spacing }) => ({ - height: ICON_SIZE, - width: ICON_SIZE, - marginEnd: spacing.sm, + // padding: spacing.xs, + alignItems: "center", + justifyContent: "center", }) -const $metadata: ThemedStyle = ({ colors, spacing }) => ({ - color: colors.textDim, - marginTop: spacing.xs, +const $metadata: ThemedStyle = ({ spacing }) => ({ + justifyContent: "space-between", + alignItems: "center", + marginBottom: spacing.xs, }) const $metadataText: ThemedStyle = ({ colors, spacing }) => ({ color: colors.textDim, - marginEnd: spacing.md, - marginBottom: spacing.xs, + marginStart: spacing.xs, }) -const $favoriteButton: ThemedStyle = ({ colors, spacing }) => ({ - borderRadius: 17, - marginTop: spacing.md, - justifyContent: "flex-start", - backgroundColor: colors.palette.neutral300, +const $actionButtons: ThemedStyle = ({ spacing }) => ({ + flexDirection: "row", + alignItems: "center", + gap: spacing.xs, +}) + +const $actionButton: ThemedStyle = ({ colors, spacing }) => ({ + backgroundColor: colors.palette.neutral200, borderColor: colors.palette.neutral300, - paddingHorizontal: spacing.md, - paddingTop: spacing.xxxs, - paddingBottom: 0, - minHeight: 32, - alignSelf: "flex-start", + // borderRadius: 20, + width: 36, + height: 20, + padding: 0, + alignItems: "center", + justifyContent: "center", }) -const $unFavoriteButton: ThemedStyle = ({ colors }) => ({ - borderColor: colors.palette.primary100, - backgroundColor: colors.palette.primary100, +const $deleteButton: ThemedStyle = ({ colors }) => ({ + backgroundColor: colors.palette.angry100, + borderColor: colors.palette.angry100, + borderRadius: 20, + width: 44, + height: 20, + paddingTop: 18, + alignItems: "center", + justifyContent: "center", }) const $emptyState: ThemedStyle = ({ spacing }) => ({ marginTop: spacing.xxl, }) - -const $emptyStateImage: ImageStyle = { - transform: [{ scaleX: isRTL ? -1 : 1 }], -} -// #endregion +// #endregion \ No newline at end of file diff --git a/app/screens/DemoShowroomScreen/DemoShowroomScreen.tsx b/app/screens/DemoShowroomScreen/DemoShowroomScreen.tsx index 28b401e..48a3720 100644 --- a/app/screens/DemoShowroomScreen/DemoShowroomScreen.tsx +++ b/app/screens/DemoShowroomScreen/DemoShowroomScreen.tsx @@ -1,4 +1,10 @@ -import { Link, RouteProp, useRoute } from "@react-navigation/native"; +// Import necessary React and React Native components and hooks +import { + Link, + RouteProp, + useFocusEffect, + useRoute, +} from "@react-navigation/native"; import { FC, ReactElement, @@ -12,237 +18,400 @@ import { ImageStyle, Platform, SectionList, - TextStyle, View, ViewStyle, - ScrollView, Alert, - TouchableOpacity, - Modal, - Dimensions, - StyleSheet, + TextStyle, } from "react-native"; import { Drawer } from "react-native-drawer-layout"; import type { ContentStyle } from "@shopify/flash-list"; +// Import custom components import { ListItem, ListView, ListViewRef, Screen, Text, - TextField, Button, + TaskModal, + showQueuedAlert, } from "../../components"; +// Import i18n utilities for internationalization import { TxKeyPath, isRTL, translate } from "@/i18n"; +// Import navigation types and theme utilities import { DemoTabParamList, DemoTabScreenProps, } from "../../navigators/DemoNavigator"; import type { Theme, ThemedStyle } from "@/theme"; import { $styles } from "@/theme"; +// Import custom hooks and utilities import { useSafeAreaInsetsStyle } from "../../utils/useSafeAreaInsetsStyle"; import * as Demos from "./demos"; import { DrawerIconButton } from "./DrawerIconButton"; import SectionListWithKeyboardAwareScrollView from "./SectionListWithKeyboardAwareScrollView"; import { useAppTheme } from "@/utils/useAppTheme"; +// Import calendar components and state management import { Calendar, CalendarList, Agenda } from "react-native-calendars"; import { useStores } from "@/models"; -import { Task } from "@/models/Task"; import { observer } from "mobx-react-lite"; +import { useHandler } from "react-native-reanimated"; import { useHeader } from "@/utils/useHeader"; -import Parse from "@/lib/Parse/parse"; - +// App logo import const logo = require("../../../assets/images/app-icon-android-adaptive-foreground.png"); +/** + * Interface defining the structure of a demo component + * @property {string} name - Name of the demo + * @property {TxKeyPath} description - Internationalization key for the description + * @property {Function} data - Function that returns an array of React elements for the demo + */ export interface Demo { name: string; description: TxKeyPath; data: ({ themed, theme }: { themed: any; theme: Theme }) => ReactElement[]; } - +/** + * Interface for demo list item props + * @property {Object} item - The demo item containing name and use cases + * @property {number} sectionIndex - Index of the current section + * @property {Function} [handleScroll] - Optional callback for handling scroll to section + */ interface DemoListItem { item: { name: string; useCases: string[] }; sectionIndex: number; handleScroll?: (sectionIndex: number, itemIndex?: number) => void; } - - - -const NativeListItem: FC = ({ - item, - sectionIndex, - handleScroll, -}) => { - const { themed } = useAppTheme(); - return ( - - handleScroll?.(sectionIndex)} - preset="bold" - style={themed($menuContainer)} - > - {item.name} - - {item.useCases.map((u, index) => ( - handleScroll?.(sectionIndex, index)} - text={u} - rightIcon={isRTL ? "caretLeft" : "caretRight"} - /> - ))} - - ); -}; - -const ShowroomListItem = Platform.select({ - default: NativeListItem, -}); +// Platform detection constant const isAndroid = Platform.OS === "android"; +/** + * Main Demo Showroom Screen component + * Displays a collection of demo components with navigation + */ export const DemoShowroomScreen: FC> = observer(function DemoShowroomScreen(_props) { + // State for controlling the drawer open/close const [open, setOpen] = useState(false); + const listRef = useRef(null); + const [taskModalVisible, setTaskModalVisible] = useState(false); + const [selectedDate, setSelectedDate] = useState(new Date()); - const [selectedDate, setSelectedDate] = useState(new Date()) - const [isTaskModalVisible, setIsTaskModalVisible] = useState(false) - const [prefilledDate, setPrefilledDate] = useState() + // Get theme and styles + const { themed, theme } = useAppTheme(); + // Get stores and their methods + const { + taskStore: { + fetchTasks, // Fetches tasks from the server + tasks, // List of all tasks + completedTasks, // List of completed tasks + createTask, // Creates a new task + pendingTasks, // List of pending tasks + getTasksForDate, + }, + } = useStores(); + // Fetch tasks when screen loads + useEffect(() => { + loadTasks(); + }, []); - const timeout = useRef>(); - const listRef = useRef(null); - const menuRef = useRef>(null); - const route = useRoute>(); - const params = route.params; + // Refresh tasks when screen comes into focus + useFocusEffect( + useCallback(() => { + loadTasks(); + }, []) + ); + const loadTasks = async () => { + try { + await fetchTasks(); + } catch (error) { + console.error("Error loading tasks:", error); - const openTaskModal = (date?: Date) => { - if (date) { - setPrefilledDate(date) - } else { - setPrefilledDate(selectedDate || new Date()) + showQueuedAlert({ + title: "Error", + message: "Failed to load tasks. Please try again.", + }); } - setIsTaskModalVisible(true) - } + }; + const generateMultiPeriodMarkedDates = useCallback(() => { + const markedDates: { [key: string]: any } = {}; - const closeTaskModal = () => { - setIsTaskModalVisible(false) - setPrefilledDate(undefined) - } + // Use the component-scoped taskColors + tasks.forEach((task, taskIndex) => { + if (task.startDate && task.dueDate) { + const startDate = new Date(task.startDate); + const dueDate = new Date(task.dueDate); + const taskColor = taskColors[taskIndex % taskColors.length]; + // Generate all dates between start and due date + const currentDate = new Date(startDate); + const dates: string[] = []; + while (currentDate <= dueDate) { + dates.push(currentDate.toISOString().split("T")[0]); + currentDate.setDate(currentDate.getDate() + 1); + } - const { themed, theme } = useAppTheme(); + // Mark each date with appropriate period properties + dates.forEach((dateStr, index) => { + if (!markedDates[dateStr]) { + markedDates[dateStr] = { periods: [] }; + } - const { - taskStore: { - fetchTasks, - tasks, - updateTask, - deleteTask, - toggleTaskCompletion, - completedTasks, - createTask, - pendingTasks, - },authenticationStore:{ - - // getEmail, - } - } = useStores(); + const isStarting = index === 0; + const isEnding = index === dates.length - 1; + markedDates[dateStr].periods.push({ + startingDay: isStarting, + endingDay: isEnding, + color: taskColor, + }); + }); + } + }); + + // Add selected date marking + if (selectedDate) { + const selectedDateStr = selectedDate.toISOString().split("T")[0]; + if (markedDates[selectedDateStr]) { + markedDates[selectedDateStr].selected = true; + markedDates[selectedDateStr].selectedColor = + theme.colors.palette.primary500; + } else { + markedDates[selectedDateStr] = { + selected: true, + selectedColor: theme.colors.palette.primary500, + periods: [], + }; + } + } - const handleDateSelect = (date: Date) => { - setSelectedDate(date) - } + return markedDates; + }, [tasks, selectedDate, theme.colors.palette.primary500]); + // Handle calendar day press + const handleDayPress = (day: { + dateString: string; + day: number; + month: number; + year: number; + timestamp: number; + }) => { + console.log("selected day", day); + setSelectedDate(new Date(day.dateString)); + }; + // Handle task creation const handleSaveTask = async (taskData: { - title: string - description: string - datetime: Date - period: "morning" | "afternoon" | "evening" - reminder: boolean + title: string; + description: string; + startDate: Date; // ← ADD THIS + dueDate: Date; // ← CHANGE from 'datetime' + taskTime: Date; // ← ADD THIS (format: "HH:MM") + period: "morning" | "afternoon" | "evening"; + reminder: boolean; }) => { try { - const newTask = { - id: Date.now().toString(), + const result = await createTask({ title: taskData.title, description: taskData.description, - dueDate: taskData.datetime.toISOString(), // Convert Date to ISO string + startDate: taskData.startDate.toISOString(), // ← ADD THIS + dueDate: taskData.dueDate.toISOString(), // ← CHANGE from 'datetime' + taskTime: taskData.taskTime, // ← ADD THIS period: taskData.period, - reminderEnabled: taskData.reminder, // Changed 'reminder' to 'reminderEnabled' to match the expected type - // isCompleted: false, // Remove if not needed in the type - // createdAt: new Date(), // Remove if not needed in the type - } + reminderEnabled: taskData.reminder, + }); - await createTask(newTask) - - Alert.alert("Success", "Task created successfully!") - - // If task was created for a different date, update selected date - if (taskData.datetime.toDateString() !== selectedDate?.toDateString()) { - setSelectedDate(taskData.datetime) + if (result) { + Alert.alert("Success", "Task created successfully!"); + setTaskModalVisible(false); } - } catch (error) { - console.error("Error saving task:", error) - Alert.alert("Error", "Failed to create task. Please try again.") - } - } - - - useEffect(() => { - fetchTasks(); - }, []); - - const toggleDrawer = useCallback(() => { - if (!open) { - setOpen(true); - } else { - setOpen(false); - } - }, [open]); - - const handleScroll = useCallback((sectionIndex: number, itemIndex = 0) => { - try { - listRef.current?.scrollToLocation({ - animated: true, - itemIndex, - sectionIndex, - viewPosition: 0.25, - }); - } catch (e) { - console.error(e); + Alert.alert("Error", "Failed to create task"); } - }, []); + }; - const scrollToIndexFailed = (info: { - index: number; - highestMeasuredFrameIndex: number; - averageItemLength: number; - }) => { - listRef.current?.getScrollResponder()?.scrollToEnd(); - timeout.current = setTimeout( - () => - listRef.current?.scrollToLocation({ - animated: true, - itemIndex: info.index, - sectionIndex: 0, - }), - 50 - ); + const scrollToIndexFailed = (info: any) => { + const wait = new Promise((resolve) => setTimeout(resolve, 500)); + wait.then(() => { + listRef.current?.scrollToIndex({ index: info.index, animated: true }); + }); }; - useEffect(() => { - return () => timeout.current && clearTimeout(timeout.current); + // Get tasks for selected date + const selectedDateTasks = getTasksForDate(selectedDate); + + // Define colors for different tasks + const taskColors = [ + "#5f9ea0", // cadet blue + "#ffa500", // orange + "#f0e68c", // khaki + "#dda0dd", // plum + "#98fb98", // pale green + "#f0b27a", // sandy brown + "#85c1e9", // sky blue + ]; + + // Demo sections - you can customize these based on your needs + const demoSections = [ + { + name: "Today's Tasks", + description: "Tasks scheduled for today", + data: [ + { + content: ( + + {selectedDateTasks.length > 0 ? ( + selectedDateTasks.map((task: any) => ( + + {task.title} + + {task.description} + + + {task.period} β€’{""} + {task.taskTime + ? ` ${new Date(task.taskTime).toLocaleTimeString()}` + : ""} + {task.startDate && + ` β€’ Start: ${new Date( + task.startDate + ).toLocaleDateString()}`} + {task.dueDate && + ` β€’ Due: ${new Date( + task.dueDate + ).toLocaleDateString()}`} + + + {/* ADD: Task period progress indicator */} + {task.startDate && task.dueDate && ( + + + Task Period:{" "} + {new Date(task.startDate).toLocaleDateString()} β†’{" "} + {new Date(task.dueDate).toLocaleDateString()} + + + + + + )} + + )) + ) : ( + + No tasks for selected date + + )} + + ), + }, + ], + }, + { + name: "Task Statistics", + description: "Overview of your tasks", + data: [ + { + content: ( + + + {completedTasks.length} + Completed + + + {pendingTasks.length} + Pending + + {/* + {tasksWithReminders.length} + With Reminders + */} + + ), + }, + ], + }, + { + name: "Quick Actions", + description: "Common task actions", + data: [ + { + content: ( + + + ) : undefined + } + RightComponent={ + + + } /> ) @@ -318,69 +442,136 @@ const $listContentContainer: ThemedStyle = ({ spacing }) => ({ const $heading: ThemedStyle = ({ spacing }) => ({ marginBottom: spacing.md, + alignItems: "center", +}) + +const $subtitle: ThemedStyle = ({ colors, spacing }) => ({ + color: colors.textDim, + marginTop: spacing.xs, +}) + +const $filterContainer: ThemedStyle = ({ spacing }) => ({ + marginBottom: spacing.md, +}) + +const $filterLabel: ThemedStyle = ({ colors, spacing }) => ({ + color: colors.textDim, + marginBottom: spacing.xs, + fontWeight: "600", +}) + +const $filterButtonRow: ThemedStyle = ({ spacing }) => ({ + + flexDirection: "row", + flexWrap: "wrap", + gap: spacing.xs, +}) + +const $filterButton: ThemedStyle = ({ colors, spacing }) => ({ + backgroundColor: colors.palette.neutral200, + borderColor: colors.palette.neutral300, + borderRadius: 16, + paddingHorizontal: spacing.sm, + paddingVertical: spacing.xs, + minHeight: 32, +}) + +const $periodFilterButton: ThemedStyle = ({ colors, spacing }) => ({ + backgroundColor: colors.palette.neutral200, + borderColor: colors.palette.neutral300, + borderRadius: 16, + paddingHorizontal: spacing.sm, + paddingVertical: spacing.xs, + minHeight: 32, + flex: 1, + maxWidth: "27%", +}) + +const $activeFilterButton: ThemedStyle = ({ colors }) => ({ + backgroundColor: colors.palette.primary100, + borderColor: colors.palette.primary300, +}) + +const $filterButtonText: ThemedStyle = ({ colors }) => ({ + color: colors.textDim, + fontSize: 12, + fontWeight: "500", +}) + +const $activeFilterButtonText: ThemedStyle = ({ colors }) => ({ + color: colors.palette.primary600, + fontWeight: "600", }) const $item: ThemedStyle = ({ colors, spacing }) => ({ padding: spacing.md, marginTop: spacing.md, - minHeight: 120, + minHeight: 100, backgroundColor: colors.palette.neutral100, }) -const $itemThumbnail: ThemedStyle = ({ spacing }) => ({ - marginTop: spacing.sm, - borderRadius: 50, - alignSelf: "flex-start", +const $completedItem: ThemedStyle = ({ colors }) => ({ + backgroundColor: colors.palette.neutral100, + opacity: 0.8, }) -const $toggle: ThemedStyle = ({ spacing }) => ({ - marginTop: spacing.md, +const $completedText: ThemedStyle = ({ colors }) => ({ + textDecorationLine: "line-through", + color: colors.textDim, }) -const $labelStyle: TextStyle = { - textAlign: "left", -} +const $description: ThemedStyle = ({ colors, spacing }) => ({ + color: colors.textDim, + marginTop: spacing.xs, + fontStyle: "italic", +}) const $iconContainer: ThemedStyle = ({ spacing }) => ({ - height: ICON_SIZE, - width: ICON_SIZE, - marginEnd: spacing.sm, + // padding: spacing.xs, + alignItems: "center", + justifyContent: "center", }) -const $metadata: ThemedStyle = ({ colors, spacing }) => ({ - color: colors.textDim, - marginTop: spacing.xs, +const $metadata: ThemedStyle = ({ spacing }) => ({ + justifyContent: "space-between", + alignItems: "center", + marginBottom: spacing.xs, }) const $metadataText: ThemedStyle = ({ colors, spacing }) => ({ color: colors.textDim, - marginEnd: spacing.md, - marginBottom: spacing.xs, + marginStart: spacing.xs, }) -const $favoriteButton: ThemedStyle = ({ colors, spacing }) => ({ - borderRadius: 17, - marginTop: spacing.md, - justifyContent: "flex-start", - backgroundColor: colors.palette.neutral300, +const $actionButtons: ThemedStyle = ({ spacing }) => ({ + flexDirection: "row", + alignItems: "center", + gap: spacing.xs, +}) + +const $actionButton: ThemedStyle = ({ colors, spacing }) => ({ + backgroundColor: colors.palette.neutral200, borderColor: colors.palette.neutral300, - paddingHorizontal: spacing.md, - paddingTop: spacing.xxxs, - paddingBottom: 0, - minHeight: 32, - alignSelf: "flex-start", + // borderRadius: 20, + width: 36, + height: 20, + padding: 0, + alignItems: "center", + justifyContent: "center", }) -const $unFavoriteButton: ThemedStyle = ({ colors }) => ({ - borderColor: colors.palette.primary100, - backgroundColor: colors.palette.primary100, +const $deleteButton: ThemedStyle = ({ colors }) => ({ + backgroundColor: colors.palette.angry100, + borderColor: colors.palette.angry100, + borderRadius: 20, + width: 44, + height: 20, + paddingTop: 18, + alignItems: "center", + justifyContent: "center", }) const $emptyState: ThemedStyle = ({ spacing }) => ({ marginTop: spacing.xxl, }) - -const $emptyStateImage: ImageStyle = { - transform: [{ scaleX: isRTL ? -1 : 1 }], -} -// #endregion +// #endregion \ No newline at end of file diff --git a/app/screens/DemoShowroomScreen/DemoShowroomScreen.tsx b/app/screens/DemoShowroomScreen/DemoShowroomScreen.tsx index 28b401e..48a3720 100644 --- a/app/screens/DemoShowroomScreen/DemoShowroomScreen.tsx +++ b/app/screens/DemoShowroomScreen/DemoShowroomScreen.tsx @@ -1,4 +1,10 @@ -import { Link, RouteProp, useRoute } from "@react-navigation/native"; +// Import necessary React and React Native components and hooks +import { + Link, + RouteProp, + useFocusEffect, + useRoute, +} from "@react-navigation/native"; import { FC, ReactElement, @@ -12,237 +18,400 @@ import { ImageStyle, Platform, SectionList, - TextStyle, View, ViewStyle, - ScrollView, Alert, - TouchableOpacity, - Modal, - Dimensions, - StyleSheet, + TextStyle, } from "react-native"; import { Drawer } from "react-native-drawer-layout"; import type { ContentStyle } from "@shopify/flash-list"; +// Import custom components import { ListItem, ListView, ListViewRef, Screen, Text, - TextField, Button, + TaskModal, + showQueuedAlert, } from "../../components"; +// Import i18n utilities for internationalization import { TxKeyPath, isRTL, translate } from "@/i18n"; +// Import navigation types and theme utilities import { DemoTabParamList, DemoTabScreenProps, } from "../../navigators/DemoNavigator"; import type { Theme, ThemedStyle } from "@/theme"; import { $styles } from "@/theme"; +// Import custom hooks and utilities import { useSafeAreaInsetsStyle } from "../../utils/useSafeAreaInsetsStyle"; import * as Demos from "./demos"; import { DrawerIconButton } from "./DrawerIconButton"; import SectionListWithKeyboardAwareScrollView from "./SectionListWithKeyboardAwareScrollView"; import { useAppTheme } from "@/utils/useAppTheme"; +// Import calendar components and state management import { Calendar, CalendarList, Agenda } from "react-native-calendars"; import { useStores } from "@/models"; -import { Task } from "@/models/Task"; import { observer } from "mobx-react-lite"; +import { useHandler } from "react-native-reanimated"; import { useHeader } from "@/utils/useHeader"; -import Parse from "@/lib/Parse/parse"; - +// App logo import const logo = require("../../../assets/images/app-icon-android-adaptive-foreground.png"); +/** + * Interface defining the structure of a demo component + * @property {string} name - Name of the demo + * @property {TxKeyPath} description - Internationalization key for the description + * @property {Function} data - Function that returns an array of React elements for the demo + */ export interface Demo { name: string; description: TxKeyPath; data: ({ themed, theme }: { themed: any; theme: Theme }) => ReactElement[]; } - +/** + * Interface for demo list item props + * @property {Object} item - The demo item containing name and use cases + * @property {number} sectionIndex - Index of the current section + * @property {Function} [handleScroll] - Optional callback for handling scroll to section + */ interface DemoListItem { item: { name: string; useCases: string[] }; sectionIndex: number; handleScroll?: (sectionIndex: number, itemIndex?: number) => void; } - - - -const NativeListItem: FC = ({ - item, - sectionIndex, - handleScroll, -}) => { - const { themed } = useAppTheme(); - return ( - - handleScroll?.(sectionIndex)} - preset="bold" - style={themed($menuContainer)} - > - {item.name} - - {item.useCases.map((u, index) => ( - handleScroll?.(sectionIndex, index)} - text={u} - rightIcon={isRTL ? "caretLeft" : "caretRight"} - /> - ))} - - ); -}; - -const ShowroomListItem = Platform.select({ - default: NativeListItem, -}); +// Platform detection constant const isAndroid = Platform.OS === "android"; +/** + * Main Demo Showroom Screen component + * Displays a collection of demo components with navigation + */ export const DemoShowroomScreen: FC> = observer(function DemoShowroomScreen(_props) { + // State for controlling the drawer open/close const [open, setOpen] = useState(false); + const listRef = useRef(null); + const [taskModalVisible, setTaskModalVisible] = useState(false); + const [selectedDate, setSelectedDate] = useState(new Date()); - const [selectedDate, setSelectedDate] = useState(new Date()) - const [isTaskModalVisible, setIsTaskModalVisible] = useState(false) - const [prefilledDate, setPrefilledDate] = useState() + // Get theme and styles + const { themed, theme } = useAppTheme(); + // Get stores and their methods + const { + taskStore: { + fetchTasks, // Fetches tasks from the server + tasks, // List of all tasks + completedTasks, // List of completed tasks + createTask, // Creates a new task + pendingTasks, // List of pending tasks + getTasksForDate, + }, + } = useStores(); + // Fetch tasks when screen loads + useEffect(() => { + loadTasks(); + }, []); - const timeout = useRef>(); - const listRef = useRef(null); - const menuRef = useRef>(null); - const route = useRoute>(); - const params = route.params; + // Refresh tasks when screen comes into focus + useFocusEffect( + useCallback(() => { + loadTasks(); + }, []) + ); + const loadTasks = async () => { + try { + await fetchTasks(); + } catch (error) { + console.error("Error loading tasks:", error); - const openTaskModal = (date?: Date) => { - if (date) { - setPrefilledDate(date) - } else { - setPrefilledDate(selectedDate || new Date()) + showQueuedAlert({ + title: "Error", + message: "Failed to load tasks. Please try again.", + }); } - setIsTaskModalVisible(true) - } + }; + const generateMultiPeriodMarkedDates = useCallback(() => { + const markedDates: { [key: string]: any } = {}; - const closeTaskModal = () => { - setIsTaskModalVisible(false) - setPrefilledDate(undefined) - } + // Use the component-scoped taskColors + tasks.forEach((task, taskIndex) => { + if (task.startDate && task.dueDate) { + const startDate = new Date(task.startDate); + const dueDate = new Date(task.dueDate); + const taskColor = taskColors[taskIndex % taskColors.length]; + // Generate all dates between start and due date + const currentDate = new Date(startDate); + const dates: string[] = []; + while (currentDate <= dueDate) { + dates.push(currentDate.toISOString().split("T")[0]); + currentDate.setDate(currentDate.getDate() + 1); + } - const { themed, theme } = useAppTheme(); + // Mark each date with appropriate period properties + dates.forEach((dateStr, index) => { + if (!markedDates[dateStr]) { + markedDates[dateStr] = { periods: [] }; + } - const { - taskStore: { - fetchTasks, - tasks, - updateTask, - deleteTask, - toggleTaskCompletion, - completedTasks, - createTask, - pendingTasks, - },authenticationStore:{ - - // getEmail, - } - } = useStores(); + const isStarting = index === 0; + const isEnding = index === dates.length - 1; + markedDates[dateStr].periods.push({ + startingDay: isStarting, + endingDay: isEnding, + color: taskColor, + }); + }); + } + }); + + // Add selected date marking + if (selectedDate) { + const selectedDateStr = selectedDate.toISOString().split("T")[0]; + if (markedDates[selectedDateStr]) { + markedDates[selectedDateStr].selected = true; + markedDates[selectedDateStr].selectedColor = + theme.colors.palette.primary500; + } else { + markedDates[selectedDateStr] = { + selected: true, + selectedColor: theme.colors.palette.primary500, + periods: [], + }; + } + } - const handleDateSelect = (date: Date) => { - setSelectedDate(date) - } + return markedDates; + }, [tasks, selectedDate, theme.colors.palette.primary500]); + // Handle calendar day press + const handleDayPress = (day: { + dateString: string; + day: number; + month: number; + year: number; + timestamp: number; + }) => { + console.log("selected day", day); + setSelectedDate(new Date(day.dateString)); + }; + // Handle task creation const handleSaveTask = async (taskData: { - title: string - description: string - datetime: Date - period: "morning" | "afternoon" | "evening" - reminder: boolean + title: string; + description: string; + startDate: Date; // ← ADD THIS + dueDate: Date; // ← CHANGE from 'datetime' + taskTime: Date; // ← ADD THIS (format: "HH:MM") + period: "morning" | "afternoon" | "evening"; + reminder: boolean; }) => { try { - const newTask = { - id: Date.now().toString(), + const result = await createTask({ title: taskData.title, description: taskData.description, - dueDate: taskData.datetime.toISOString(), // Convert Date to ISO string + startDate: taskData.startDate.toISOString(), // ← ADD THIS + dueDate: taskData.dueDate.toISOString(), // ← CHANGE from 'datetime' + taskTime: taskData.taskTime, // ← ADD THIS period: taskData.period, - reminderEnabled: taskData.reminder, // Changed 'reminder' to 'reminderEnabled' to match the expected type - // isCompleted: false, // Remove if not needed in the type - // createdAt: new Date(), // Remove if not needed in the type - } + reminderEnabled: taskData.reminder, + }); - await createTask(newTask) - - Alert.alert("Success", "Task created successfully!") - - // If task was created for a different date, update selected date - if (taskData.datetime.toDateString() !== selectedDate?.toDateString()) { - setSelectedDate(taskData.datetime) + if (result) { + Alert.alert("Success", "Task created successfully!"); + setTaskModalVisible(false); } - } catch (error) { - console.error("Error saving task:", error) - Alert.alert("Error", "Failed to create task. Please try again.") - } - } - - - useEffect(() => { - fetchTasks(); - }, []); - - const toggleDrawer = useCallback(() => { - if (!open) { - setOpen(true); - } else { - setOpen(false); - } - }, [open]); - - const handleScroll = useCallback((sectionIndex: number, itemIndex = 0) => { - try { - listRef.current?.scrollToLocation({ - animated: true, - itemIndex, - sectionIndex, - viewPosition: 0.25, - }); - } catch (e) { - console.error(e); + Alert.alert("Error", "Failed to create task"); } - }, []); + }; - const scrollToIndexFailed = (info: { - index: number; - highestMeasuredFrameIndex: number; - averageItemLength: number; - }) => { - listRef.current?.getScrollResponder()?.scrollToEnd(); - timeout.current = setTimeout( - () => - listRef.current?.scrollToLocation({ - animated: true, - itemIndex: info.index, - sectionIndex: 0, - }), - 50 - ); + const scrollToIndexFailed = (info: any) => { + const wait = new Promise((resolve) => setTimeout(resolve, 500)); + wait.then(() => { + listRef.current?.scrollToIndex({ index: info.index, animated: true }); + }); }; - useEffect(() => { - return () => timeout.current && clearTimeout(timeout.current); + // Get tasks for selected date + const selectedDateTasks = getTasksForDate(selectedDate); + + // Define colors for different tasks + const taskColors = [ + "#5f9ea0", // cadet blue + "#ffa500", // orange + "#f0e68c", // khaki + "#dda0dd", // plum + "#98fb98", // pale green + "#f0b27a", // sandy brown + "#85c1e9", // sky blue + ]; + + // Demo sections - you can customize these based on your needs + const demoSections = [ + { + name: "Today's Tasks", + description: "Tasks scheduled for today", + data: [ + { + content: ( + + {selectedDateTasks.length > 0 ? ( + selectedDateTasks.map((task: any) => ( + + {task.title} + + {task.description} + + + {task.period} β€’{""} + {task.taskTime + ? ` ${new Date(task.taskTime).toLocaleTimeString()}` + : ""} + {task.startDate && + ` β€’ Start: ${new Date( + task.startDate + ).toLocaleDateString()}`} + {task.dueDate && + ` β€’ Due: ${new Date( + task.dueDate + ).toLocaleDateString()}`} + + + {/* ADD: Task period progress indicator */} + {task.startDate && task.dueDate && ( + + + Task Period:{" "} + {new Date(task.startDate).toLocaleDateString()} β†’{" "} + {new Date(task.dueDate).toLocaleDateString()} + + + + + + )} + + )) + ) : ( + + No tasks for selected date + + )} + + ), + }, + ], + }, + { + name: "Task Statistics", + description: "Overview of your tasks", + data: [ + { + content: ( + + + {completedTasks.length} + Completed + + + {pendingTasks.length} + Pending + + {/* + {tasksWithReminders.length} + With Reminders + */} + + ), + }, + ], + }, + { + name: "Quick Actions", + description: "Common task actions", + data: [ + { + content: ( + + + ) +} + +// News article card component - simplified +const NewsArticleCard: FC<{ + article: NewsArticle + onPressFavorite: () => void + isFavorite: boolean +}> = ({ article, onPressFavorite, isFavorite }) => { + const { themed } = useAppTheme() + + const imageSource = useMemo(() => + article.image_url ? { uri: article.image_url } : undefined + , [article.image_url]) + + const accessibilityProps = useMemo( + (): AccessibilityProps => ({ + accessibilityLabel: article.title, + accessibilityActions: [{ name: "longpress", label: "Toggle favorite" }], + onAccessibilityAction: ({ nativeEvent }) => { + if (nativeEvent.actionName === "longpress") { + onPressFavorite() + } + }, + accessibilityRole: "button", + accessibilityHint: "Tap to view article, long press to toggle favorite", + }), + [article.title, onPressFavorite, isFavorite], + ) + + const handlePressCard = useCallback(() => { + openLinkInBrowser(article.link) + }, [article.link]) + + return ( + + + + {article.primaryCountry && ( + + )} + + } + content={article.title} + RightComponent={ + imageSource ? ( + + ) : undefined + } + FooterComponent={ + + {article.shortDescription && ( + + )} + + + } + /> + ) +} + +// Styles remain the same +const $listContentContainer: ThemedStyle = ({ spacing }) => ({ + paddingHorizontal: spacing.lg, + paddingTop: spacing.lg + spacing.xl, + paddingBottom: spacing.lg, +}) + +const $heading: ThemedStyle = ({ spacing }) => ({ + marginBottom: spacing.md, +}) + +const $subtitle: ThemedStyle = ({ colors, spacing }) => ({ + color: colors.textDim, + marginTop: spacing.xs, +}) + +const $resultsCount: ThemedStyle = ({ colors, spacing }) => ({ + color: colors.textDim, + marginTop: spacing.sm, +}) + +const $item: ThemedStyle = ({ colors, spacing }) => ({ + padding: spacing.md, + marginTop: spacing.md, + minHeight: 140, + backgroundColor: colors.palette.neutral100, +}) + +const $itemThumbnail: ThemedStyle = ({ spacing }) => ({ + marginTop: spacing.sm, + borderRadius: 8, + alignSelf: "flex-start", + width: 80, + height: 80, +}) + +const $description: ThemedStyle = ({ colors, spacing }) => ({ + color: colors.textDim, + marginTop: spacing.xs, marginBottom: spacing.sm, }) -const $tagline: ThemedStyle = ({ spacing }) => ({ - marginBottom: spacing.xxl, +const $toggle: ThemedStyle = ({ spacing }) => ({ + marginTop: spacing.md, }) -const $description: ThemedStyle = ({ spacing }) => ({ - marginBottom: spacing.lg, +const $labelStyle: TextStyle = { + textAlign: "left", +} + +const $iconContainer: ThemedStyle = ({ spacing }) => ({ + height: ICON_SIZE, + width: ICON_SIZE, + marginEnd: spacing.sm, }) -const $sectionTitle: ThemedStyle = ({ spacing }) => ({ - marginTop: spacing.xxl, +const $metadata: ThemedStyle = ({ spacing }) => ({ + marginTop: spacing.xs, }) -const $logoContainer: ThemedStyle = ({ spacing }) => ({ +const $metadataText: ThemedStyle = ({ colors, spacing }) => ({ + color: colors.textDim, marginEnd: spacing.md, - flexWrap: "wrap", - alignContent: "center", - alignSelf: "stretch", + marginBottom: spacing.xs, }) -const $logo: ImageStyle = { - height: 38, - width: 38, -} +const $favoriteButton: ThemedStyle = ({ colors, spacing }) => ({ + borderRadius: 17, + marginTop: spacing.md, + justifyContent: "flex-start", + backgroundColor: colors.palette.neutral300, + borderColor: colors.palette.neutral300, + paddingHorizontal: spacing.md, + paddingTop: spacing.xxxs, + paddingBottom: 0, + minHeight: 32, + alignSelf: "flex-start", +}) + +const $unFavoriteButton: ThemedStyle = ({ colors }) => ({ + borderColor: colors.palette.primary100, + backgroundColor: colors.palette.primary100, +}) + +const $loadingFooter: ThemedStyle = ({ spacing }) => ({ + paddingVertical: spacing.lg, + alignItems: "center", +}) + +const $loadingText: ThemedStyle = ({ colors, spacing }) => ({ + color: colors.textDim, + marginTop: spacing.sm, +}) + +const $emptyState: ThemedStyle = ({ spacing }) => ({ + marginTop: spacing.xxl, +}) + +const $emptyStateImage: ImageStyle = { + transform: [{ scaleX: isRTL ? -1 : 1 }], +} \ No newline at end of file diff --git a/app/services/api/api.ts b/app/services/api/api.ts index 1ea4c2c..20cd6e6 100644 --- a/app/services/api/api.ts +++ b/app/services/api/api.ts @@ -8,8 +8,66 @@ import { ApiResponse, ApisauceInstance, create } from "apisauce" import Config from "../../config" import { GeneralApiProblem, getGeneralApiProblem } from "./apiProblem" -import type { ApiConfig, ApiFeedResponse } from "./api.types" -import type { EpisodeSnapshotIn } from "../../models/Episode" +import type { ApiConfig } from "./api.types" + +// NewsData.io API types +export interface NewsArticle { + article_id: string + title: string + link: string + keywords: string[] + creator: string[] + description: string + content: string + pubDate: string + pubDateTZ: string + image_url: string | null + video_url: string | null + source_id: string + source_name: string + source_priority: number + source_url: string + source_icon: string + language: string + country: string[] + category: string[] + ai_tag?: string + sentiment?: string + ai_summary?: string +} + +export interface NewsDataResponse { + status: string + totalResults: number + results: NewsArticle[] + nextPage?: string +} + +export interface NewsSource { + id: string + name: string + url: string + category: string[] + language: string[] + country: string[] + description: string + status: string +} + +export interface NewsSourcesResponse { + status: string + results: NewsSource[] +} + +export interface NewsSearchParams { + q?: string + country?: string + category?: string + language?: string + from_date?: string + to_date?: string + page?: string +} /** * Configuring the apisauce instance. @@ -19,12 +77,22 @@ export const DEFAULT_API_CONFIG: ApiConfig = { timeout: 10000, } +/** + * NewsData.io API configuration + */ +export const NEWSDATA_API_CONFIG = { + baseURL: "https://newsdata.io/api/1/", + apiKey: "pub_ba0b6114ab7a4c4bb4c770a6fe1de3fd", + timeout: 15000, +} + /** * Manages all requests to the API. You can use this class to build out * various requests that you need to call from your backend API. */ export class Api { apisauce: ApisauceInstance + newsDataApi: ApisauceInstance config: ApiConfig /** @@ -39,34 +107,131 @@ export class Api { Accept: "application/json", }, }) + + // Set up NewsData.io API instance + this.newsDataApi = create({ + baseURL: NEWSDATA_API_CONFIG.baseURL, + timeout: NEWSDATA_API_CONFIG.timeout, + headers: { + Accept: "application/json", + }, + }) + } + + /** + * Gets latest news articles from NewsData.io + */ + async getLatestNews(params: NewsSearchParams = {}): Promise<{ kind: "ok"; articles: NewsArticle[]; totalResults: number; nextPage?: string } | GeneralApiProblem> { + // Build query parameters + const queryParams = { + apikey: NEWSDATA_API_CONFIG.apiKey, + ...params, + } + + const response: ApiResponse = await this.newsDataApi.get("latest", queryParams) + + if (!response.ok) { + const problem = getGeneralApiProblem(response) + if (problem) return problem + } + + try { + const rawData = response.data + + if (rawData?.status !== "success") { + return { kind: "bad-data" } + } + + return { + kind: "ok", + articles: rawData.results || [], + totalResults: rawData.totalResults || 0, + nextPage: rawData.nextPage, + } + } catch (e) { + if (__DEV__ && e instanceof Error) { + console.error(`Bad data: ${e.message}\n${response.data}`, e.stack) + } + return { kind: "bad-data" } + } + } + + /** + * Gets global healthcare news + */ + async getGlobalHealthcareNews(page?: string): Promise<{ kind: "ok"; articles: NewsArticle[]; totalResults: number; nextPage?: string } | GeneralApiProblem> { + return this.getLatestNews({ + category: "health", + language: "en", + ...(page && { page }), + }) + } + + /** + * Gets healthcare news for a specific country + */ + async getCountryHealthcareNews(country: string, page?: string): Promise<{ kind: "ok"; articles: NewsArticle[]; totalResults: number; nextPage?: string } | GeneralApiProblem> { + return this.getLatestNews({ + country, + category: "health", + language: "en", + ...(page && { page }), + }) + } + + /** + * Gets Ghana-specific healthcare news + */ + async getGhanaHealthcareNews(page?: string): Promise<{ kind: "ok"; articles: NewsArticle[]; totalResults: number; nextPage?: string } | GeneralApiProblem> { + return this.getCountryHealthcareNews("GH", page) + } + + /** + * Search for specific healthcare topics + */ + async searchHealthcareNews(query: string, country?: string, page?: string): Promise<{ kind: "ok"; articles: NewsArticle[]; totalResults: number; nextPage?: string } | GeneralApiProblem> { + return this.getLatestNews({ + q: query, + category: "health", + language: "en", + ...(country && { country }), + ...(page && { page }), + }) } /** - * Gets a list of recent React Native Radio episodes. + * Gets archived healthcare news within a date range */ - async getEpisodes(): Promise<{ kind: "ok"; episodes: EpisodeSnapshotIn[] } | GeneralApiProblem> { - // make the api call - const response: ApiResponse = await this.apisauce.get( - `api.json?rss_url=https%3A%2F%2Ffeeds.simplecast.com%2FhEI_f9Dx`, - ) + async getHealthcareNewsArchive(fromDate: string, toDate: string, query?: string, country?: string): Promise<{ kind: "ok"; articles: NewsArticle[]; totalResults: number; nextPage?: string } | GeneralApiProblem> { + const queryParams = { + apikey: NEWSDATA_API_CONFIG.apiKey, + from_date: fromDate, + to_date: toDate, + language: "en", + ...(query && { q: query }), + ...(country && { country }), + } + + const response: ApiResponse = await this.newsDataApi.get("archive", queryParams) - // the typical ways to die when calling an api if (!response.ok) { const problem = getGeneralApiProblem(response) if (problem) return problem } - // transform the data into the format we are expecting try { const rawData = response.data - // This is where we transform the data into the shape we expect for our MST model. - const episodes: EpisodeSnapshotIn[] = - rawData?.items.map((raw) => ({ - ...raw, - })) ?? [] + if (rawData?.status !== "success") { + return { kind: "bad-data" } + } - return { kind: "ok", episodes } + return { + kind: "ok", + articles: rawData.results || [], + totalResults: rawData.totalResults || 0, + nextPage: rawData.nextPage, + } } catch (e) { if (__DEV__ && e instanceof Error) { console.error(`Bad data: ${e.message}\n${response.data}`, e.stack) @@ -74,7 +239,50 @@ export class Api { return { kind: "bad-data" } } } + + /** + * Gets available news sources + */ + async getNewsSources(country?: string, category?: string): Promise<{ kind: "ok"; sources: NewsSource[] } | GeneralApiProblem> { + const queryParams = { + apikey: NEWSDATA_API_CONFIG.apiKey, + ...(country && { country }), + ...(category && { category }), + } + + const response: ApiResponse = await this.newsDataApi.get("sources", queryParams) + + if (!response.ok) { + const problem = getGeneralApiProblem(response) + if (problem) return problem + } + + try { + const rawData = response.data + + if (rawData?.status !== "success") { + return { kind: "bad-data" } + } + + return { + kind: "ok", + sources: rawData.results || [], + } + } catch (e) { + if (__DEV__ && e instanceof Error) { + console.error(`Bad data: ${e.message}\n${response.data}`, e.stack) + } + return { kind: "bad-data" } + } + } + + /** + * Gets Ghana healthcare news sources + */ + async getGhanaHealthcareSources(): Promise<{ kind: "ok"; sources: NewsSource[] } | GeneralApiProblem> { + return this.getNewsSources("GH", "health") + } } // Singleton instance of the API for convenience -export const api = new Api() +export const api = new Api() \ No newline at end of file diff --git a/app/services/api/api.types.ts b/app/services/api/api.types.ts index fef3afd..aded0dc 100644 --- a/app/services/api/api.types.ts +++ b/app/services/api/api.types.ts @@ -2,37 +2,6 @@ * These types indicate the shape of the data you expect to receive from your * API endpoint, assuming it's a JSON object like we have. */ -export interface EpisodeItem { - title: string - pubDate: string - link: string - guid: string - author: string - thumbnail: string - description: string - content: string - enclosure: { - link: string - type: string - length: number - duration: number - rating: { scheme: string; value: string } - } - categories: string[] -} - -export interface ApiFeedResponse { - status: string - feed: { - url: string - title: string - link: string - author: string - description: string - image: string - } - items: EpisodeItem[] -} /** * The options used to configure apisauce. @@ -47,4 +16,4 @@ export interface ApiConfig { * Milliseconds before we timeout the request. */ timeout: number -} +} \ No newline at end of file diff --git a/assets/icons/newspaper.png b/assets/icons/newspaper.png new file mode 100644 index 0000000000000000000000000000000000000000..8dcfd848879f91fecc308599fbe0c752f8c1ea90 GIT binary patch literal 5395 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 btD)F>+Z6=Fe0DMVD>hdy8tIi^K!^VqKNr_} literal 0 HcmV?d00001 diff --git a/assets/icons/schedule.png b/assets/icons/schedule.png new file mode 100644 index 0000000000000000000000000000000000000000..ff3f111546b894a0380d8db0e190dc82682cc6c3 GIT binary patch literal 15851 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 literal 0 HcmV?d00001 From f2c681b7555d658ca8a8d912b7b92b1d2933ab8d Mon Sep 17 00:00:00 2001 From: neweracy Date: Mon, 4 Aug 2025 14:39:35 +0000 Subject: [PATCH 24/44] feat: Add initial deployment workflow and text file --- .github/workflow/deploy.yml | 0 .txt | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 .github/workflow/deploy.yml create mode 100644 .txt diff --git a/.github/workflow/deploy.yml b/.github/workflow/deploy.yml new file mode 100644 index 0000000..e69de29 diff --git a/.txt b/.txt new file mode 100644 index 0000000..e69de29 From 41465b3f8f024633f3e3b63fff63d7dd39586a96 Mon Sep 17 00:00:00 2001 From: neweracy Date: Mon, 4 Aug 2025 14:44:21 +0000 Subject: [PATCH 25/44] feat: Add initial test and deployment workflow configuration --- .github/workflows/testAndDeploy.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .github/workflows/testAndDeploy.yml diff --git a/.github/workflows/testAndDeploy.yml b/.github/workflows/testAndDeploy.yml new file mode 100644 index 0000000..e69de29 From ac50c68d27ae186f3314af54f6918154011ffa2b Mon Sep 17 00:00:00 2001 From: neweracy Date: Mon, 4 Aug 2025 14:44:21 +0000 Subject: [PATCH 26/44] feat: Add initial test and deployment workflow configuration --- .github/workflows/testAndDeploy.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .github/workflows/testAndDeploy.yml diff --git a/.github/workflows/testAndDeploy.yml b/.github/workflows/testAndDeploy.yml new file mode 100644 index 0000000..e69de29 From a2330ff1b840d1985f8066cd5572af678fe6867b Mon Sep 17 00:00:00 2001 From: neweracy Date: Mon, 4 Aug 2025 14:50:24 +0000 Subject: [PATCH 27/44] feat: Add GitHub Actions workflow for testing and deploying Ignite React Native app --- .github/workflows/testAndDeploy.yml | 382 ++++++++++++++++++++++++++++ 1 file changed, 382 insertions(+) diff --git a/.github/workflows/testAndDeploy.yml b/.github/workflows/testAndDeploy.yml index e69de29..e5b9bc7 100644 --- a/.github/workflows/testAndDeploy.yml +++ b/.github/workflows/testAndDeploy.yml @@ -0,0 +1,382 @@ +name: Test and Deploy Ignite React Native App + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop ] + workflow_dispatch: + inputs: + deploy_environment: + description: 'Deployment environment' + required: true + default: 'staging' + type: choice + options: + - staging + - production + +env: + NODE_VERSION: '18' + JAVA_VERSION: '17' + XCODE_VERSION: '15.0' + +jobs: + # Install dependencies and cache them + setup: + runs-on: ubuntu-latest + outputs: + cache-key: ${{ steps.cache-key.outputs.key }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Generate cache key + id: cache-key + run: echo "key=node-modules-${{ hashFiles('**/package-lock.json') }}" >> $GITHUB_OUTPUT + + - name: Cache node modules + uses: actions/cache@v3 + with: + path: node_modules + key: ${{ steps.cache-key.outputs.key }} + restore-keys: | + node-modules- + + - name: Install dependencies + run: | + npm ci + npx react-native doctor || true + + # Lint and Type Check + lint-and-typecheck: + needs: setup + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Restore node modules cache + uses: actions/cache@v3 + with: + path: node_modules + key: ${{ needs.setup.outputs.cache-key }} + + - name: Install dependencies + run: npm ci + + - name: Run ESLint + run: npm run lint + + - name: Run TypeScript check + run: npx tsc --noEmit + + - name: Check formatting + run: npm run format:check || npx prettier --check "**/*.{js,jsx,ts,tsx,json,md}" + + # Run tests + test: + needs: setup + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Restore node modules cache + uses: actions/cache@v3 + with: + path: node_modules + key: ${{ needs.setup.outputs.cache-key }} + + - name: Install dependencies + run: npm ci + + - name: Run unit tests + run: npm test -- --coverage --watchAll=false + + - name: Upload coverage reports + uses: codecov/codecov-action@v3 + with: + file: ./coverage/lcov.info + fail_ci_if_error: false + + # Security audit + security-audit: + needs: setup + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Run security audit + run: npm audit --audit-level=high + + - name: Check for vulnerabilities + run: npx audit-ci --moderate + + # Build Android APK/AAB + build-android: + needs: [lint-and-typecheck, test] + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' || github.event_name == 'workflow_dispatch' + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: ${{ env.JAVA_VERSION }} + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + + - name: Restore node modules cache + uses: actions/cache@v3 + with: + path: node_modules + key: ${{ needs.setup.outputs.cache-key }} + + - name: Install dependencies + run: npm ci + + - name: Cache Gradle + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + android/.gradle + key: gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + + - name: Make gradlew executable + run: chmod +x android/gradlew + + - name: Clean Android build + run: | + cd android + ./gradlew clean + + - name: Build Android Debug APK + if: github.ref == 'refs/heads/develop' + run: | + cd android + ./gradlew assembleDebug + + - name: Build Android Release APK + if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch' + env: + ANDROID_KEYSTORE: ${{ secrets.ANDROID_KEYSTORE }} + ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} + ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} + run: | + cd android + ./gradlew assembleRelease + + - name: Upload Android APK + uses: actions/upload-artifact@v4 + with: + name: android-apk + path: | + android/app/build/outputs/apk/**/*.apk + retention-days: 30 + + # Build iOS IPA + build-ios: + needs: [lint-and-typecheck, test] + runs-on: macos-latest + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' || github.event_name == 'workflow_dispatch' + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Setup Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: ${{ env.XCODE_VERSION }} + + - name: Restore node modules cache + uses: actions/cache@v3 + with: + path: node_modules + key: ${{ needs.setup.outputs.cache-key }} + + - name: Install dependencies + run: npm ci + + - name: Cache CocoaPods + uses: actions/cache@v3 + with: + path: ios/Pods + key: pods-${{ hashFiles('**/Podfile.lock') }} + restore-keys: | + pods- + + - name: Install CocoaPods dependencies + run: | + cd ios + bundle install + bundle exec pod install + + - name: Build iOS Debug + if: github.ref == 'refs/heads/develop' + run: | + cd ios + xcodebuild -workspace Dooit.xcworkspace \ + -scheme Dooit \ + -configuration Debug \ + -destination 'generic/platform=iOS Simulator' \ + build + + - name: Build iOS Release + if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch' + env: + IOS_CERTIFICATE: ${{ secrets.IOS_CERTIFICATE }} + IOS_CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }} + IOS_PROVISIONING_PROFILE: ${{ secrets.IOS_PROVISIONING_PROFILE }} + run: | + cd ios + # Setup certificates and provisioning profiles here + xcodebuild -workspace Dooit.xcworkspace \ + -scheme Dooit \ + -configuration Release \ + -archivePath Dooit.xcarchive \ + archive + + - name: Upload iOS Archive + if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch' + uses: actions/upload-artifact@v4 + with: + name: ios-archive + path: ios/Dooit.xcarchive + retention-days: 30 + + # Deploy to staging/production + deploy: + needs: [build-android, build-ios] + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch' + environment: + name: ${{ github.event.inputs.deploy_environment || 'staging' }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download Android APK + uses: actions/download-artifact@v4 + with: + name: android-apk + path: ./android-build + + - name: Download iOS Archive + uses: actions/download-artifact@v4 + with: + name: ios-archive + path: ./ios-build + + - name: Deploy to App Store Connect + if: contains(github.event.inputs.deploy_environment, 'production') || github.ref == 'refs/heads/main' + env: + APP_STORE_CONNECT_API_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY }} + APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} + APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }} + run: | + echo "Deploying to App Store Connect..." + # Add your App Store Connect deployment commands here + + - name: Deploy to Google Play Console + if: contains(github.event.inputs.deploy_environment, 'production') || github.ref == 'refs/heads/main' + env: + GOOGLE_PLAY_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT }} + run: | + echo "Deploying to Google Play Console..." + # Add your Google Play Console deployment commands here + + - name: Deploy to Firebase App Distribution + if: contains(github.event.inputs.deploy_environment, 'staging') + env: + FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }} + FIREBASE_APP_ID_ANDROID: ${{ secrets.FIREBASE_APP_ID_ANDROID }} + FIREBASE_APP_ID_IOS: ${{ secrets.FIREBASE_APP_ID_IOS }} + run: | + npm install -g firebase-tools + echo "Deploying to Firebase App Distribution..." + # Add Firebase App Distribution commands here + + # Send notifications + notify: + needs: [deploy] + runs-on: ubuntu-latest + if: always() && (github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch') + steps: + - name: Send Slack notification + if: always() + uses: 8398a7/action-slack@v3 + with: + status: ${{ job.status }} + channel: '#mobile-deployments' + text: | + ${{ github.event_name == 'workflow_dispatch' && 'Manual' || 'Automatic' }} deployment ${{ job.status }} + Branch: ${{ github.ref_name }} + Environment: ${{ github.event.inputs.deploy_environment || 'staging' }} + Commit: ${{ github.sha }} + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + + - name: Send email notification + if: failure() + uses: dawidd6/action-send-mail@v3 + with: + server_address: smtp.gmail.com + server_port: 587 + username: ${{ secrets.EMAIL_USERNAME }} + password: ${{ secrets.EMAIL_PASSWORD }} + subject: "❌ Dooit App Deployment Failed" + body: | + The deployment of Dooit app has failed. + + Branch: ${{ github.ref_name }} + Commit: ${{ github.sha }} + Workflow: ${{ github.workflow }} + + Please check the GitHub Actions logs for more details. + to: ${{ secrets.NOTIFICATION_EMAIL }} + from: ${{ secrets.EMAIL_USERNAME }} \ No newline at end of file From 56f87e77fbc2d8fe76bca2417dfc1a3100a9f5ce Mon Sep 17 00:00:00 2001 From: neweracy Date: Mon, 4 Aug 2025 14:50:24 +0000 Subject: [PATCH 28/44] feat: Add GitHub Actions workflow for testing and deploying Ignite React Native app --- .github/workflows/testAndDeploy.yml | 382 ++++++++++++++++++++++++++++ 1 file changed, 382 insertions(+) diff --git a/.github/workflows/testAndDeploy.yml b/.github/workflows/testAndDeploy.yml index e69de29..e5b9bc7 100644 --- a/.github/workflows/testAndDeploy.yml +++ b/.github/workflows/testAndDeploy.yml @@ -0,0 +1,382 @@ +name: Test and Deploy Ignite React Native App + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop ] + workflow_dispatch: + inputs: + deploy_environment: + description: 'Deployment environment' + required: true + default: 'staging' + type: choice + options: + - staging + - production + +env: + NODE_VERSION: '18' + JAVA_VERSION: '17' + XCODE_VERSION: '15.0' + +jobs: + # Install dependencies and cache them + setup: + runs-on: ubuntu-latest + outputs: + cache-key: ${{ steps.cache-key.outputs.key }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Generate cache key + id: cache-key + run: echo "key=node-modules-${{ hashFiles('**/package-lock.json') }}" >> $GITHUB_OUTPUT + + - name: Cache node modules + uses: actions/cache@v3 + with: + path: node_modules + key: ${{ steps.cache-key.outputs.key }} + restore-keys: | + node-modules- + + - name: Install dependencies + run: | + npm ci + npx react-native doctor || true + + # Lint and Type Check + lint-and-typecheck: + needs: setup + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Restore node modules cache + uses: actions/cache@v3 + with: + path: node_modules + key: ${{ needs.setup.outputs.cache-key }} + + - name: Install dependencies + run: npm ci + + - name: Run ESLint + run: npm run lint + + - name: Run TypeScript check + run: npx tsc --noEmit + + - name: Check formatting + run: npm run format:check || npx prettier --check "**/*.{js,jsx,ts,tsx,json,md}" + + # Run tests + test: + needs: setup + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Restore node modules cache + uses: actions/cache@v3 + with: + path: node_modules + key: ${{ needs.setup.outputs.cache-key }} + + - name: Install dependencies + run: npm ci + + - name: Run unit tests + run: npm test -- --coverage --watchAll=false + + - name: Upload coverage reports + uses: codecov/codecov-action@v3 + with: + file: ./coverage/lcov.info + fail_ci_if_error: false + + # Security audit + security-audit: + needs: setup + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Run security audit + run: npm audit --audit-level=high + + - name: Check for vulnerabilities + run: npx audit-ci --moderate + + # Build Android APK/AAB + build-android: + needs: [lint-and-typecheck, test] + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' || github.event_name == 'workflow_dispatch' + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: ${{ env.JAVA_VERSION }} + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + + - name: Restore node modules cache + uses: actions/cache@v3 + with: + path: node_modules + key: ${{ needs.setup.outputs.cache-key }} + + - name: Install dependencies + run: npm ci + + - name: Cache Gradle + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + android/.gradle + key: gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + + - name: Make gradlew executable + run: chmod +x android/gradlew + + - name: Clean Android build + run: | + cd android + ./gradlew clean + + - name: Build Android Debug APK + if: github.ref == 'refs/heads/develop' + run: | + cd android + ./gradlew assembleDebug + + - name: Build Android Release APK + if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch' + env: + ANDROID_KEYSTORE: ${{ secrets.ANDROID_KEYSTORE }} + ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} + ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} + run: | + cd android + ./gradlew assembleRelease + + - name: Upload Android APK + uses: actions/upload-artifact@v4 + with: + name: android-apk + path: | + android/app/build/outputs/apk/**/*.apk + retention-days: 30 + + # Build iOS IPA + build-ios: + needs: [lint-and-typecheck, test] + runs-on: macos-latest + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' || github.event_name == 'workflow_dispatch' + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Setup Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: ${{ env.XCODE_VERSION }} + + - name: Restore node modules cache + uses: actions/cache@v3 + with: + path: node_modules + key: ${{ needs.setup.outputs.cache-key }} + + - name: Install dependencies + run: npm ci + + - name: Cache CocoaPods + uses: actions/cache@v3 + with: + path: ios/Pods + key: pods-${{ hashFiles('**/Podfile.lock') }} + restore-keys: | + pods- + + - name: Install CocoaPods dependencies + run: | + cd ios + bundle install + bundle exec pod install + + - name: Build iOS Debug + if: github.ref == 'refs/heads/develop' + run: | + cd ios + xcodebuild -workspace Dooit.xcworkspace \ + -scheme Dooit \ + -configuration Debug \ + -destination 'generic/platform=iOS Simulator' \ + build + + - name: Build iOS Release + if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch' + env: + IOS_CERTIFICATE: ${{ secrets.IOS_CERTIFICATE }} + IOS_CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }} + IOS_PROVISIONING_PROFILE: ${{ secrets.IOS_PROVISIONING_PROFILE }} + run: | + cd ios + # Setup certificates and provisioning profiles here + xcodebuild -workspace Dooit.xcworkspace \ + -scheme Dooit \ + -configuration Release \ + -archivePath Dooit.xcarchive \ + archive + + - name: Upload iOS Archive + if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch' + uses: actions/upload-artifact@v4 + with: + name: ios-archive + path: ios/Dooit.xcarchive + retention-days: 30 + + # Deploy to staging/production + deploy: + needs: [build-android, build-ios] + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch' + environment: + name: ${{ github.event.inputs.deploy_environment || 'staging' }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download Android APK + uses: actions/download-artifact@v4 + with: + name: android-apk + path: ./android-build + + - name: Download iOS Archive + uses: actions/download-artifact@v4 + with: + name: ios-archive + path: ./ios-build + + - name: Deploy to App Store Connect + if: contains(github.event.inputs.deploy_environment, 'production') || github.ref == 'refs/heads/main' + env: + APP_STORE_CONNECT_API_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY }} + APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} + APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }} + run: | + echo "Deploying to App Store Connect..." + # Add your App Store Connect deployment commands here + + - name: Deploy to Google Play Console + if: contains(github.event.inputs.deploy_environment, 'production') || github.ref == 'refs/heads/main' + env: + GOOGLE_PLAY_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT }} + run: | + echo "Deploying to Google Play Console..." + # Add your Google Play Console deployment commands here + + - name: Deploy to Firebase App Distribution + if: contains(github.event.inputs.deploy_environment, 'staging') + env: + FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }} + FIREBASE_APP_ID_ANDROID: ${{ secrets.FIREBASE_APP_ID_ANDROID }} + FIREBASE_APP_ID_IOS: ${{ secrets.FIREBASE_APP_ID_IOS }} + run: | + npm install -g firebase-tools + echo "Deploying to Firebase App Distribution..." + # Add Firebase App Distribution commands here + + # Send notifications + notify: + needs: [deploy] + runs-on: ubuntu-latest + if: always() && (github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch') + steps: + - name: Send Slack notification + if: always() + uses: 8398a7/action-slack@v3 + with: + status: ${{ job.status }} + channel: '#mobile-deployments' + text: | + ${{ github.event_name == 'workflow_dispatch' && 'Manual' || 'Automatic' }} deployment ${{ job.status }} + Branch: ${{ github.ref_name }} + Environment: ${{ github.event.inputs.deploy_environment || 'staging' }} + Commit: ${{ github.sha }} + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + + - name: Send email notification + if: failure() + uses: dawidd6/action-send-mail@v3 + with: + server_address: smtp.gmail.com + server_port: 587 + username: ${{ secrets.EMAIL_USERNAME }} + password: ${{ secrets.EMAIL_PASSWORD }} + subject: "❌ Dooit App Deployment Failed" + body: | + The deployment of Dooit app has failed. + + Branch: ${{ github.ref_name }} + Commit: ${{ github.sha }} + Workflow: ${{ github.workflow }} + + Please check the GitHub Actions logs for more details. + to: ${{ secrets.NOTIFICATION_EMAIL }} + from: ${{ secrets.EMAIL_USERNAME }} \ No newline at end of file From 2cd422822fb2f1fac41bf9542effba3d4fb5c551 Mon Sep 17 00:00:00 2001 From: neweracy Date: Mon, 4 Aug 2025 14:55:57 +0000 Subject: [PATCH 29/44] feat: Add .env to .gitignore for improved configuration management --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index cb8b804..25aca85 100644 --- a/.gitignore +++ b/.gitignore @@ -85,6 +85,7 @@ web-build/ # Configurations !env.js +.env /coverage From f819bdae6afe7503f28e9a0fa5e60dc31820a6c6 Mon Sep 17 00:00:00 2001 From: neweracy Date: Mon, 4 Aug 2025 14:55:57 +0000 Subject: [PATCH 30/44] feat: Add .env to .gitignore for improved configuration management --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index cb8b804..25aca85 100644 --- a/.gitignore +++ b/.gitignore @@ -85,6 +85,7 @@ web-build/ # Configurations !env.js +.env /coverage From 61ceb5fe3babf6d2a19f11ba0d95aaf6191ca71b Mon Sep 17 00:00:00 2001 From: neweracy Date: Mon, 4 Aug 2025 16:07:14 +0000 Subject: [PATCH 31/44] fix: Update dependency installation method and cache key for improved performance --- .github/workflows/testAndDeploy.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/testAndDeploy.yml b/.github/workflows/testAndDeploy.yml index e5b9bc7..eeedb0f 100644 --- a/.github/workflows/testAndDeploy.yml +++ b/.github/workflows/testAndDeploy.yml @@ -39,7 +39,7 @@ jobs: - name: Generate cache key id: cache-key - run: echo "key=node-modules-${{ hashFiles('**/package-lock.json') }}" >> $GITHUB_OUTPUT + run: echo "key=node-modules-${{ hashFiles('**/package.json') }}" >> $GITHUB_OUTPUT - name: Cache node modules uses: actions/cache@v3 @@ -51,7 +51,7 @@ jobs: - name: Install dependencies run: | - npm ci + npm install npx react-native doctor || true # Lint and Type Check @@ -75,7 +75,7 @@ jobs: key: ${{ needs.setup.outputs.cache-key }} - name: Install dependencies - run: npm ci + run: npm install - name: Run ESLint run: npm run lint @@ -107,7 +107,7 @@ jobs: key: ${{ needs.setup.outputs.cache-key }} - name: Install dependencies - run: npm ci + run: npm install - name: Run unit tests run: npm test -- --coverage --watchAll=false @@ -169,7 +169,7 @@ jobs: key: ${{ needs.setup.outputs.cache-key }} - name: Install dependencies - run: npm ci + run: npm install - name: Cache Gradle uses: actions/cache@v3 @@ -240,7 +240,7 @@ jobs: key: ${{ needs.setup.outputs.cache-key }} - name: Install dependencies - run: npm ci + run: npm install - name: Cache CocoaPods uses: actions/cache@v3 From eadca0c71559f85040bab640164bbd8a5378c106 Mon Sep 17 00:00:00 2001 From: neweracy Date: Mon, 4 Aug 2025 16:07:14 +0000 Subject: [PATCH 32/44] fix: Update dependency installation method and cache key for improved performance --- .github/workflows/testAndDeploy.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/testAndDeploy.yml b/.github/workflows/testAndDeploy.yml index e5b9bc7..eeedb0f 100644 --- a/.github/workflows/testAndDeploy.yml +++ b/.github/workflows/testAndDeploy.yml @@ -39,7 +39,7 @@ jobs: - name: Generate cache key id: cache-key - run: echo "key=node-modules-${{ hashFiles('**/package-lock.json') }}" >> $GITHUB_OUTPUT + run: echo "key=node-modules-${{ hashFiles('**/package.json') }}" >> $GITHUB_OUTPUT - name: Cache node modules uses: actions/cache@v3 @@ -51,7 +51,7 @@ jobs: - name: Install dependencies run: | - npm ci + npm install npx react-native doctor || true # Lint and Type Check @@ -75,7 +75,7 @@ jobs: key: ${{ needs.setup.outputs.cache-key }} - name: Install dependencies - run: npm ci + run: npm install - name: Run ESLint run: npm run lint @@ -107,7 +107,7 @@ jobs: key: ${{ needs.setup.outputs.cache-key }} - name: Install dependencies - run: npm ci + run: npm install - name: Run unit tests run: npm test -- --coverage --watchAll=false @@ -169,7 +169,7 @@ jobs: key: ${{ needs.setup.outputs.cache-key }} - name: Install dependencies - run: npm ci + run: npm install - name: Cache Gradle uses: actions/cache@v3 @@ -240,7 +240,7 @@ jobs: key: ${{ needs.setup.outputs.cache-key }} - name: Install dependencies - run: npm ci + run: npm install - name: Cache CocoaPods uses: actions/cache@v3 From 431ef1fc298626d712d337d9cc0e7f13d1dff06e Mon Sep 17 00:00:00 2001 From: neweracy Date: Mon, 4 Aug 2025 18:07:51 +0000 Subject: [PATCH 33/44] feat: Update environment variables for server URLs and add Android Google client ID --- .env | 5 +++-- app/navigators/AppNavigator.tsx | 1 + app/screens/ChooseAuthScreen.tsx | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.env b/.env index c867add..ff2404c 100644 --- a/.env +++ b/.env @@ -5,8 +5,8 @@ EXPO_PUBLIC_APP_ID=duapod EXPO_PUBLIC_MASTER_KEY=75a68740-11ed-4ef1-87ca-a4b45435b328 EXPO_PUBLIC_JAVASCRIPT_KEY=e2c7fef1-971d-4a92-829b-9f8ad6545423 -EXPO_PUBLIC_SERVER_URL=http://localhost:1337/parse -PARSE_DEV_SERVER_URL=http://localhost:1337/parse +EXPO_PUBLIC_SERVER_URL=http://192.168.100.99:1337/parse +PARSE_DEV_SERVER_URL=http://192.168.100.99:1337/parse EXPO_PUBLIC_PORT_NUMBER=1337 PARSE_APP_NAME=DuaPOD @@ -20,6 +20,7 @@ EXPO_PUBLIC_CLOUDINARY_UPLOAD_PRESET=ml_default # The EXPO_PUBLIC_GOOGLE_CLIENT_ID is used for web and Android. EXPO_PUBLIC_GOOGLE_CLIENT_ID=688661758272-c7c0tfnjfmuc9073noea1rbh8qig01tq.apps.googleusercontent.com EXPO_PUBLIC_IOS_GOOGLE_CLIENT_ID=688661758272-3pgi5k91pr4hdc8acf731kkc53g4ct0c.apps.googleusercontent.com +EXPO_PUBLIC_ANDROID_GOOGLE_CLIENT_ID=688661758272-s78f2kijrvj8tbda9naksao61t1k1lmd.apps.googleusercontent.com # These variables are used to configure the Expo app and connect it to the Arkesel API. diff --git a/app/navigators/AppNavigator.tsx b/app/navigators/AppNavigator.tsx index 18eeaeb..ef4f468 100644 --- a/app/navigators/AppNavigator.tsx +++ b/app/navigators/AppNavigator.tsx @@ -5,6 +5,7 @@ * and a "main" flow which the user will use once logged in. */ import { NavigationContainer, NavigatorScreenParams } from "@react-navigation/native" +import React from "react" import { createNativeStackNavigator, NativeStackScreenProps } from "@react-navigation/native-stack" import { observer } from "mobx-react-lite" import * as Screens from "@/screens" diff --git a/app/screens/ChooseAuthScreen.tsx b/app/screens/ChooseAuthScreen.tsx index 9361505..4c238dc 100644 --- a/app/screens/ChooseAuthScreen.tsx +++ b/app/screens/ChooseAuthScreen.tsx @@ -111,7 +111,7 @@ export const ChooseAuthScreen: FC = observer( { leftIcon: "back", title: "Welcome to Dooit", - onLeftPress: () => navigation.goBack(), + onLeftPress: () => navigation.navigate("Welcome"), }, [navigation] ); From 6e660e5183a71bf8d3e4e411c2c609a4a243c78f Mon Sep 17 00:00:00 2001 From: neweracy Date: Mon, 4 Aug 2025 18:07:51 +0000 Subject: [PATCH 34/44] feat: Update environment variables for server URLs and add Android Google client ID --- .env | 5 +++-- app/navigators/AppNavigator.tsx | 1 + app/screens/ChooseAuthScreen.tsx | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.env b/.env index c867add..ff2404c 100644 --- a/.env +++ b/.env @@ -5,8 +5,8 @@ EXPO_PUBLIC_APP_ID=duapod EXPO_PUBLIC_MASTER_KEY=75a68740-11ed-4ef1-87ca-a4b45435b328 EXPO_PUBLIC_JAVASCRIPT_KEY=e2c7fef1-971d-4a92-829b-9f8ad6545423 -EXPO_PUBLIC_SERVER_URL=http://localhost:1337/parse -PARSE_DEV_SERVER_URL=http://localhost:1337/parse +EXPO_PUBLIC_SERVER_URL=http://192.168.100.99:1337/parse +PARSE_DEV_SERVER_URL=http://192.168.100.99:1337/parse EXPO_PUBLIC_PORT_NUMBER=1337 PARSE_APP_NAME=DuaPOD @@ -20,6 +20,7 @@ EXPO_PUBLIC_CLOUDINARY_UPLOAD_PRESET=ml_default # The EXPO_PUBLIC_GOOGLE_CLIENT_ID is used for web and Android. EXPO_PUBLIC_GOOGLE_CLIENT_ID=688661758272-c7c0tfnjfmuc9073noea1rbh8qig01tq.apps.googleusercontent.com EXPO_PUBLIC_IOS_GOOGLE_CLIENT_ID=688661758272-3pgi5k91pr4hdc8acf731kkc53g4ct0c.apps.googleusercontent.com +EXPO_PUBLIC_ANDROID_GOOGLE_CLIENT_ID=688661758272-s78f2kijrvj8tbda9naksao61t1k1lmd.apps.googleusercontent.com # These variables are used to configure the Expo app and connect it to the Arkesel API. diff --git a/app/navigators/AppNavigator.tsx b/app/navigators/AppNavigator.tsx index 18eeaeb..ef4f468 100644 --- a/app/navigators/AppNavigator.tsx +++ b/app/navigators/AppNavigator.tsx @@ -5,6 +5,7 @@ * and a "main" flow which the user will use once logged in. */ import { NavigationContainer, NavigatorScreenParams } from "@react-navigation/native" +import React from "react" import { createNativeStackNavigator, NativeStackScreenProps } from "@react-navigation/native-stack" import { observer } from "mobx-react-lite" import * as Screens from "@/screens" diff --git a/app/screens/ChooseAuthScreen.tsx b/app/screens/ChooseAuthScreen.tsx index 9361505..4c238dc 100644 --- a/app/screens/ChooseAuthScreen.tsx +++ b/app/screens/ChooseAuthScreen.tsx @@ -111,7 +111,7 @@ export const ChooseAuthScreen: FC = observer( { leftIcon: "back", title: "Welcome to Dooit", - onLeftPress: () => navigation.goBack(), + onLeftPress: () => navigation.navigate("Welcome"), }, [navigation] ); From 26dccde129fd4488cd41cc3f2d0751eb8f1056dd Mon Sep 17 00:00:00 2001 From: neweracy Date: Mon, 4 Aug 2025 18:21:48 +0000 Subject: [PATCH 35/44] feat: Add .env to .gitignore to prevent sensitive information from being tracked --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 25aca85..91efb40 100644 --- a/.gitignore +++ b/.gitignore @@ -96,3 +96,5 @@ web-build/ !.yarn/releases !.yarn/sdks !.yarn/versions +.env +.env From 8bc90bd0f06c641b7a3eed4d5f8cc859cb4723ff Mon Sep 17 00:00:00 2001 From: neweracy Date: Mon, 4 Aug 2025 18:21:48 +0000 Subject: [PATCH 36/44] feat: Add .env to .gitignore to prevent sensitive information from being tracked --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 25aca85..91efb40 100644 --- a/.gitignore +++ b/.gitignore @@ -96,3 +96,5 @@ web-build/ !.yarn/releases !.yarn/sdks !.yarn/versions +.env +.env From 3ea8be086f87b74acf97c9410ee16f82aaadda08 Mon Sep 17 00:00:00 2001 From: neweracy Date: Tue, 5 Aug 2025 05:11:34 +0000 Subject: [PATCH 37/44] fix --- .env | 4 ++-- .gitignore | 4 ++++ app.json | 3 ++- app/models/NewsArticle.ts | 0 app/models/NewsStore.ts | 0 google-services.json | 29 +++++++++++++++++++++++++++++ key.txt.pub | 1 - 7 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 app/models/NewsArticle.ts create mode 100644 app/models/NewsStore.ts create mode 100644 google-services.json diff --git a/.env b/.env index 36641ca..caae66d 100644 --- a/.env +++ b/.env @@ -5,8 +5,8 @@ EXPO_PUBLIC_APP_ID=duapod EXPO_PUBLIC_MASTER_KEY=75a68740-11ed-4ef1-87ca-a4b45435b328 EXPO_PUBLIC_JAVASCRIPT_KEY=e2c7fef1-971d-4a92-829b-9f8ad6545423 -EXPO_PUBLIC_SERVER_URL=http://localhost:1337/parse -PARSE_DEV_SERVER_URL=http://localhost:1337/parse +EXPO_PUBLIC_SERVER_URL=https://dooitserver.duckdns.org/parse +PARSE_DEV_SERVER_URL=https://dooitserver.duckdns.org/parse EXPO_PUBLIC_PORT_NUMBER=1337 PARSE_APP_NAME=DuaPOD diff --git a/.gitignore b/.gitignore index 25aca85..3e2cafe 100644 --- a/.gitignore +++ b/.gitignore @@ -96,3 +96,7 @@ web-build/ !.yarn/releases !.yarn/sdks !.yarn/versions +.env + +key.txt.pub +key.txt.pub diff --git a/app.json b/app.json index 9dcb3e5..5af76d4 100644 --- a/app.json +++ b/app.json @@ -27,7 +27,8 @@ "allowBackup": false, "splash": { "backgroundColor": "#fff" - } + }, + "googleServicesFile": "./google-services.json" }, "ios": { "icon": "./assets/images/app-icon-ios.png", diff --git a/app/models/NewsArticle.ts b/app/models/NewsArticle.ts new file mode 100644 index 0000000..e69de29 diff --git a/app/models/NewsStore.ts b/app/models/NewsStore.ts new file mode 100644 index 0000000..e69de29 diff --git a/google-services.json b/google-services.json new file mode 100644 index 0000000..40d2468 --- /dev/null +++ b/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "56964832577", + "project_id": "dooit-6e563", + "storage_bucket": "dooit-6e563.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:56964832577:android:832649cf05baf356c895c0", + "android_client_info": { + "package_name": "com.dooit" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyD6Pmp0uuBbzDywjw4-2el8hgHYZiZdvBU" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/key.txt.pub b/key.txt.pub index d7db6d7..e69de29 100644 --- a/key.txt.pub +++ b/key.txt.pub @@ -1 +0,0 @@ -ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAwrFs7Pek6GlkFGcoenktx1d6SmBB0rSbACagCU1un7 adelaidemariaansahofei@Adelaides-MacBook-Pro-8.local From 718493462c625e43278ffa0b84d0d4d349fe9ce5 Mon Sep 17 00:00:00 2001 From: neweracy Date: Tue, 5 Aug 2025 05:14:54 +0000 Subject: [PATCH 38/44] feat: Update .gitignore to include Firebase configuration files --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 3e2cafe..8b60fcf 100644 --- a/.gitignore +++ b/.gitignore @@ -97,6 +97,8 @@ web-build/ !.yarn/sdks !.yarn/versions .env - +google-services.json +firebase-service-account.json +dooit-6e563-firebase-adminsdk-fbsvc-a82e6fd47e.json key.txt.pub key.txt.pub From 36e2eaad8d3fe5b60e5286547a455a038c02ab87 Mon Sep 17 00:00:00 2001 From: neweracy Date: Tue, 5 Aug 2025 05:18:46 +0000 Subject: [PATCH 39/44] fix: Remove duplicate entry for Firebase service account JSON in .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8b60fcf..bc2e90b 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,4 @@ firebase-service-account.json dooit-6e563-firebase-adminsdk-fbsvc-a82e6fd47e.json key.txt.pub key.txt.pub +dooit-6e563-firebase-adminsdk-fbsvc-a82e6fd47e.json From 16333987663a91fbf39194d55d3eb69873af0996 Mon Sep 17 00:00:00 2001 From: neweracy Date: Tue, 5 Aug 2025 05:19:40 +0000 Subject: [PATCH 40/44] feat: Update .gitignore to include additional environment configuration files --- .gitignore | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index bc2e90b..e9410f4 100644 --- a/.gitignore +++ b/.gitignore @@ -85,7 +85,10 @@ web-build/ # Configurations !env.js -.env +.env.*.js +.env.local +.env.*.local + /coverage @@ -96,7 +99,7 @@ web-build/ !.yarn/releases !.yarn/sdks !.yarn/versions -.env + google-services.json firebase-service-account.json dooit-6e563-firebase-adminsdk-fbsvc-a82e6fd47e.json From 1b125d3b6566765cf31ac1ab72edd7061942bbf0 Mon Sep 17 00:00:00 2001 From: neweracy Date: Tue, 5 Aug 2025 05:50:34 +0000 Subject: [PATCH 41/44] feat(Notifications): Enhance notification handling with error management and setup hooks --- app/screens/DemoDebugScreen.tsx | 162 +++++++++++++++++- app/services/notifications/notification.ts | 183 +++++++++++++++++---- bun.lockb | Bin 614669 -> 614677 bytes 3 files changed, 311 insertions(+), 34 deletions(-) diff --git a/app/screens/DemoDebugScreen.tsx b/app/screens/DemoDebugScreen.tsx index 6bbbbe5..f9e5ab7 100644 --- a/app/screens/DemoDebugScreen.tsx +++ b/app/screens/DemoDebugScreen.tsx @@ -1,4 +1,4 @@ -import { FC, useCallback, useMemo } from "react"; +import { FC, useCallback, useMemo, useState, useEffect } from "react"; import * as Application from "expo-application"; import { LayoutAnimation, @@ -8,6 +8,7 @@ import { useColorScheme, View, ViewStyle, + Alert, } from "react-native"; import { Button, ListItem, Screen, Text } from "../components"; import { DemoTabScreenProps } from "../navigators/DemoNavigator"; @@ -16,6 +17,12 @@ import { $styles } from "../theme"; import { isRTL } from "@/i18n"; import { useStores } from "../models"; import { useAppTheme } from "@/utils/useAppTheme"; +import { + registerForPushNotificationsAsync, + sendPushNotification, + setupNotificationListeners, + NotificationPermissionError +} from "../services/notifications/notification"; // Adjust path as needed /** * @param {string} url - The URL to open in the browser. @@ -31,14 +38,45 @@ const usingHermes = export const DemoDebugScreen: FC> = function DemoDebugScreen(_props) { - const { setThemeContextOverride, themeContext, themed } = useAppTheme(); + const { setThemeContextOverride, themeContext, themed, theme } = useAppTheme(); const { authenticationStore: { logout }, } = useStores(); + // Push notification state + const [expoPushToken, setExpoPushToken] = useState(''); + const [notificationError, setNotificationError] = useState(null); + const [isNotificationLoading, setIsNotificationLoading] = useState(false); + // @ts-expect-error const usingFabric = global.nativeFabricUIManager != null; + // Setup push notifications + useEffect(() => { + const initializeNotifications = async () => { + setIsNotificationLoading(true); + try { + const token = await registerForPushNotificationsAsync( + theme.colors.palette.primary500 + ); + setExpoPushToken(token || ''); + setNotificationError(null); + } catch (error) { + setNotificationError(error as NotificationPermissionError); + console.error('Push notification setup failed:', error); + } finally { + setIsNotificationLoading(false); + } + }; + + initializeNotifications(); + + // Set up notification listeners + const cleanup = setupNotificationListeners(); + + return cleanup; + }, [theme.colors.palette.primary500]); + const demoReactotron = useMemo( () => async () => { if (__DEV__) { @@ -50,12 +88,13 @@ export const DemoDebugScreen: FC { @@ -70,6 +109,55 @@ export const DemoDebugScreen: FC { + if (!expoPushToken) { + Alert.alert('Error', 'No push token available. Make sure notifications are enabled.'); + return; + } + + try { + const success = await sendPushNotification( + expoPushToken, + 'Test Notification πŸš€', + 'This is a test notification from the debug screen!', + { + screen: 'debug', + timestamp: new Date().toISOString(), + testData: 'Hello from Dooit!' + } + ); + + if (success) { + Alert.alert('Success', 'Test notification sent! Check your notification tray.'); + } else { + Alert.alert('Error', 'Failed to send test notification. Check console for details.'); + } + } catch (error) { + Alert.alert('Error', `Failed to send notification: ${error}`); + } + }, [expoPushToken]); + + const getNotificationStatus = () => { + if (isNotificationLoading) return 'Setting up...'; + if (notificationError) { + switch (notificationError.code) { + case 'PERMISSION_DENIED': + return 'Permission denied'; + case 'NO_DEVICE': + return 'Simulator (not supported)'; + case 'NO_PROJECT_ID': + return 'Config error'; + case 'TOKEN_ERROR': + return 'Token error'; + default: + return 'Setup failed'; + } + } + if (expoPushToken) return 'Ready'; + return 'Not available'; + }; + return (