diff --git a/packages/good-design/src/apps/usertasks/ManagerTaskCards.stories.tsx b/packages/good-design/src/apps/usertasks/ManagerTaskCards.stories.tsx
new file mode 100644
index 00000000..5f0087ac
--- /dev/null
+++ b/packages/good-design/src/apps/usertasks/ManagerTaskCards.stories.tsx
@@ -0,0 +1,23 @@
+import * as React from "react";
+import { ClaimerTasksCompact } from "./managerTaskCard";
+import { NativeBaseProvider, Box } from "native-base";
+import { action } from "@storybook/addon-actions";
+import { SAMPLE_TASKS } from "./mockData";
+
+export default {
+ title: "UserTasks/ClaimerTasksCompact",
+ component: ClaimerTasksCompact
+};
+
+export const AsModal = () => (
+
+
+
+
+
+);
diff --git a/packages/good-design/src/apps/usertasks/managerTaskCard.tsx b/packages/good-design/src/apps/usertasks/managerTaskCard.tsx
new file mode 100644
index 00000000..bc99318a
--- /dev/null
+++ b/packages/good-design/src/apps/usertasks/managerTaskCard.tsx
@@ -0,0 +1,650 @@
+import React, { useState, useEffect, useMemo, useCallback, FC } from "react";
+import { VStack, HStack, Pressable, Box, Spinner, useToast } from "native-base";
+import { Linking } from "react-native";
+import AsyncStorage from "@react-native-async-storage/async-storage";
+import moment from "moment";
+import { TransText, TransButton } from "../../core/layout";
+import { SAMPLE_TASKS } from "./mockData";
+import BasicStyledModal from "../../core/web3/modals/BasicStyledModal";
+import { noop } from "lodash";
+
+
+export interface TaskReward {
+ type: "points" | "tokens" | "badge";
+ amount?: number;
+ description: string;
+}
+
+export const useClaimerTasks = () => {
+ const [dismissedTasks, setDismissedTasks] = useState<{ [key: string]: number }>({});
+ const [completedTasks, setCompletedTasks] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [dismissing, setDismissing] = useState(false);
+ const toast = useToast();
+
+ useEffect(() => {
+ const loadData = async () => {
+ try {
+ const [dismissed, completed] = await Promise.all([
+ AsyncStorage.getItem("dismissed_claimer_tasks"),
+ AsyncStorage.getItem("completed_claimer_tasks")
+ ]);
+ if (dismissed) setDismissedTasks(JSON.parse(dismissed));
+ if (completed) setCompletedTasks(JSON.parse(completed));
+ } catch (error) {
+ console.warn("Failed to load task data:", error);
+ toast.show({
+ title: "Error",
+ description: "Failed to load tasks. Please try again."
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+ void loadData();
+ }, []);
+
+ const dismissTask = async (taskId: string, skipToast = false) => {
+ const now = Date.now();
+ const updated = { ...dismissedTasks, [taskId]: now };
+ setDismissedTasks(updated);
+ try {
+ await AsyncStorage.setItem("dismissed_claimer_tasks", JSON.stringify(updated));
+ if (!skipToast) {
+ toast.show({
+ title: "Task Dismissed",
+ description: "Task has been dismissed."
+ });
+ }
+ } catch (e) {
+ console.warn("Failed to store dismissed task:", e);
+ toast.show({
+ title: "Error",
+ description: "Failed to dismiss task. Please try again."
+ });
+ }
+ };
+
+ const dismissAllTasks = async (taskIds: string[], onTaskDismiss?: (taskId: string) => void) => {
+ setDismissing(true);
+ const now = Date.now();
+ const updated = taskIds.reduce((acc, taskId) => ({ ...acc, [taskId]: now }), dismissedTasks);
+ setDismissedTasks(updated);
+ try {
+ await AsyncStorage.setItem("dismissed_claimer_tasks", JSON.stringify(updated));
+ taskIds.forEach(taskId => {
+ if (onTaskDismiss) onTaskDismiss(taskId);
+ });
+ toast.show({
+ title: "Tasks Dismissed",
+ description: "All tasks have been dismissed."
+ });
+ } catch (e) {
+ console.warn("Failed to store dismissed tasks:", e);
+ toast.show({
+ title: "Error",
+ description: "Failed to dismiss tasks. Please try again."
+ });
+ } finally {
+ setDismissing(false);
+ }
+ };
+
+ const completeTask = useCallback(async (taskId: string) => {
+ if (completedTasks.includes(taskId)) {
+ toast.show({
+ title: "Task Already Completed",
+ description: "This task has already been completed."
+ });
+ return;
+ }
+ const updated = [...completedTasks, taskId];
+ setCompletedTasks(updated);
+ try {
+ await AsyncStorage.setItem("completed_claimer_tasks", JSON.stringify(updated));
+ toast.show({
+ title: "Success",
+ description: "Task completed successfully!"
+ });
+ } catch (e) {
+ console.warn("Failed to store completed task:", e);
+ toast.show({
+ title: "Error",
+ description: "Failed to mark task as completed. Please try again."
+ });
+ }
+ }, [completedTasks, toast]);
+
+ const availableTasks = useMemo(() => {
+ const now = moment();
+ const fourDaysDuration = moment.duration(4, 'days');
+
+ return SAMPLE_TASKS.filter(task => {
+ const startTime = moment(task.duration.startDate);
+ const endTime = moment(task.duration.endDate);
+
+ if (now.isBefore(startTime) || now.isAfter(endTime)) return false;
+
+ if (completedTasks.includes(task.id)) return false;
+
+ const dismissed = dismissedTasks[task.id];
+ if (dismissed) {
+ const dismissedTime = moment(dismissed);
+ if (now.diff(dismissedTime) < fourDaysDuration.asMilliseconds()) return false;
+ }
+
+ return true;
+ });
+ }, [dismissedTasks, completedTasks]);
+
+ const mainTask = availableTasks.find(t => t.priority === "main");
+ const secondaryTasks = availableTasks.filter(t => t.priority === "secondary");
+
+ return {
+ mainTask,
+ secondaryTasks,
+ loading,
+ dismissing,
+ hasActiveTasks: availableTasks.length > 0,
+ dismissTask,
+ dismissAllTasks,
+ completeTask
+ };
+};
+
+
+export interface ClaimerTask {
+ type: "noTasks" | "activeTasks" | "secondaryTasks" | "modalContent" | "learn";
+ taskId?: string;
+ content?: string;
+ id: string;
+ title: string;
+ description: string;
+ category: "social" | "donation" | "referral" | "engagement";
+ reward?: TaskReward;
+ duration: { startDate: string; endDate: string };
+ actionUrl?: string;
+ icon?: string;
+ rewardAmount?: string;
+ rewardColor?: string;
+ priority?: "main" | "secondary";
+}
+
+interface ManagerTaskProps {
+ type: ClaimerTask["type"];
+ task?: ClaimerTask;
+ isPending: boolean;
+ customTitle?: string;
+ onClose?: () => void;
+ onPress?: () => void;
+ fontStyles?: any;
+}
+
+const TaskModalContent: React.FC<{ task: ClaimerTask; fontStyles?: any }> = ({ task, fontStyles }) => {
+ const { subHeading, subContent } = fontStyles ?? {};
+
+ return (
+
+ {/* Icon */}
+
+
+
+
+
+
+ {/* Task Title */}
+
+
+ {/* Task Description */}
+
+
+ {/* Reward */}
+ {task.rewardAmount && (
+
+
+
+
+ )}
+
+ );
+};
+
+export const ManagerTask: FC = ({
+ onClose = noop,
+ task,
+ isPending,
+ customTitle,
+ onPress,
+ fontStyles,
+ ...props
+}) => {
+ if (!task) return null;
+
+ return (
+ }
+ withOverlay="dark"
+ withCloseButton={true}
+ footer={
+ onPress && (
+
+ )
+ }
+ />
+ );
+};
+
+interface ClaimerTasksCardProps {
+ tasks?: ClaimerTask[];
+ onTaskComplete?: (taskId: string) => void;
+ onTaskDismiss?: (taskId: string) => void;
+ fontStyles?: any;
+ ContentComponent?: any;
+ showAsModal?: boolean;
+}
+
+const ManagerModalContent = ({ content }: { content: string }) => (
+
+);
+
+const ManagerContentDismiss = () => (
+
+
+
+
+
+);
+
+const ManagerSecondaryTasks = () => (
+
+
+
+);
+
+const ManagerDoNextTasks = () => (
+
+
+
+
+);
+
+const DefaultContentComponent = {
+ noTasks: ManagerContentDismiss,
+ activeTasks: ManagerDoNextTasks,
+ secondaryTasks: ManagerSecondaryTasks,
+ modalContent: ManagerModalContent
+};
+
+
+const TasksCardContent: React.FC<{
+ mainTask?: ClaimerTask;
+ secondaryTasks: ClaimerTask[];
+ dismissing: boolean;
+ fontStyles?: any;
+ ContentComponent: any;
+ onTaskSelect: (task: ClaimerTask) => void;
+ onDismissAll: () => void;
+ ManagerModalContent: any;
+}> = ({
+ mainTask,
+ secondaryTasks,
+ dismissing,
+ fontStyles,
+ ContentComponent,
+ onTaskSelect,
+ onDismissAll,
+ ManagerModalContent
+}) => {
+ const { secondaryTasks: ManagerSecondaryTasks } = ContentComponent;
+
+ return (
+
+ {/* Main Task */}
+ {mainTask && (
+
+ {/* Icon */}
+
+
+
+
+
+
+ {/* Task Title */}
+
+
+
+
+
+
+
+ {/* CTA Button */}
+
+ onTaskSelect(mainTask)}
+ testID="main-task-button"
+ variant="ghost"
+ />
+
+
+ )}
+
+ {/* Secondary Tasks */}
+ {secondaryTasks.length > 0 && (
+
+
+
+
+
+ {secondaryTasks.map(task => (
+ onTaskSelect(task)}
+ _pressed={{ bg: "gray.50" }}
+ px={4}
+ py={4}
+ bg="white"
+ borderRadius="xl"
+ borderWidth={1}
+ borderColor="gray.200"
+ testID={`task-${task.id}`}
+ w="100%"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+ )}
+
+ {/* Footer Button */}
+
+
+
+
+ );
+};
+
+export const ClaimerTasksCard: React.FC = ({
+ onTaskComplete,
+ onTaskDismiss,
+ fontStyles,
+ ContentComponent = DefaultContentComponent,
+ showAsModal = true
+}) => {
+ const { mainTask, secondaryTasks, loading, dismissing, hasActiveTasks, dismissAllTasks, completeTask } = useClaimerTasks();
+ const [selectedTask, setSelectedTask] = useState(null);
+ const [showMainModal, setShowMainModal] = useState(false);
+
+ const ManagerModalContent = ContentComponent.modalContent;
+ const ManagerContentDismiss = ContentComponent.noTasks;
+ const ManagerDoNextTasks = ContentComponent.activeTasks;
+
+ const handleDismissAll = async () => {
+ const taskIds = [...(mainTask ? [mainTask.id] : []), ...secondaryTasks.map(task => task.id)];
+ if (taskIds.length > 0) {
+ await dismissAllTasks(taskIds, onTaskDismiss);
+ setShowMainModal(false);
+ }
+ };
+
+ const openTask = useCallback(async (task: ClaimerTask) => {
+ if (task.actionUrl) {
+ try {
+ await Linking.openURL(task.actionUrl);
+ await completeTask(task.id);
+ if (onTaskComplete) onTaskComplete(task.id);
+ setSelectedTask(null);
+ } catch (error) {
+ console.warn("Failed to open task URL:", error);
+ }
+ }
+ }, [completeTask, onTaskComplete]);
+
+ const handleTaskSelect = (task: ClaimerTask) => {
+ if (task.id === mainTask?.id) {
+ void openTask(task);
+ } else {
+ setSelectedTask(task);
+ }
+ };
+
+ useEffect(() => {
+ if (!loading && hasActiveTasks && showAsModal) {
+ setShowMainModal(true);
+ }
+ }, [loading, hasActiveTasks, showAsModal]);
+
+ if (loading) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ if (!hasActiveTasks) {
+ return (
+ {/* handle close - maybe navigate away? */}}
+ title=""
+ body={}
+ withOverlay="dark"
+ withCloseButton={true}
+ />
+ );
+ }
+
+ if (!showAsModal) {
+ return (
+
+
+
+
+
+
+
+
+ {/* Individual Task Modal */}
+ {selectedTask && (
+ setSelectedTask(null)}
+ onPress={() => void openTask(selectedTask)}
+ fontStyles={fontStyles}
+ />
+ )}
+
+ );
+ }
+
+ return (
+ <>
+ {/* Main Tasks Modal */}
+ setShowMainModal(false)}
+ title=""
+ body={
+
+
+
+
+ }
+ withOverlay="dark"
+ withCloseButton={true}
+ />
+
+ {/* Individual Task Modal */}
+ {selectedTask && (
+ setSelectedTask(null)}
+ onPress={() => void openTask(selectedTask)}
+ fontStyles={fontStyles}
+ />
+ )}
+ >
+ );
+};
+
+export { ClaimerTasksCard as ClaimerTasksCompact };
\ No newline at end of file
diff --git a/packages/good-design/src/apps/usertasks/mockData.ts b/packages/good-design/src/apps/usertasks/mockData.ts
new file mode 100644
index 00000000..2e9be43c
--- /dev/null
+++ b/packages/good-design/src/apps/usertasks/mockData.ts
@@ -0,0 +1,44 @@
+import { ClaimerTask } from "./managerTaskCard";
+
+export const SAMPLE_TASKS: ClaimerTask[] = [
+ {
+ id: "treasury-vote",
+ title: "Have your say in the Treasury Proposal",
+ description: "Your vote decides where 10 MG$ goes.",
+ category: "engagement",
+ priority: "main",
+ reward: {type: "tokens", amount: 10, description: "Cast My Vote"},
+ duration: {startDate: "2026-02-01", endDate: "2026-02-28"},
+ actionUrl: "https://www.gooddollar.org/",
+ icon: "📦",
+ type: "learn"
+ },
+ {
+ id: "goodcollective-donate",
+ title: "Donate to GoodCollective",
+ description: "Support a verified cause",
+ category: "donation",
+ priority: "secondary",
+ reward: {type: "tokens", amount: -50, description: "Donate 50G$"},
+ duration: {startDate: "2026-02-01", endDate: "2026-02-28"},
+ actionUrl: "https://www.gooddollar.org/",
+ icon: "❤️",
+ rewardAmount: "-50 G$",
+ rewardColor: "red.500",
+ type: "learn"
+ },
+ {
+ id: "invite-friend",
+ title: "Invite a friend & earn",
+ description: "You both get rewarded!",
+ category: "referral",
+ priority: "secondary",
+ reward: {type: "tokens", amount: 20, description: "Invite Friend"},
+ duration: {startDate: "2026-02-01", endDate: "2026-02-28"},
+ actionUrl: "https://www.gooddollar.org/",
+ icon: "👥",
+ rewardAmount: "+20 G$",
+ rewardColor: "green.500",
+ type: "learn"
+ }
+];
\ No newline at end of file