@umituz/react-native-settings 4.20.61 → 4.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -60
- package/src/domains/gamification/README.md +343 -0
- package/src/domains/gamification/components/AchievementCard.tsx +142 -0
- package/src/domains/gamification/components/AchievementItem.tsx +182 -0
- package/src/domains/gamification/components/AchievementToast.tsx +122 -0
- package/src/domains/gamification/components/GamificationScreen/AchievementsList.tsx +84 -0
- package/src/domains/gamification/components/GamificationScreen/Header.tsx +29 -0
- package/src/domains/gamification/components/GamificationScreen/StatsGrid.tsx +51 -0
- package/src/domains/gamification/components/GamificationScreen/index.tsx +111 -0
- package/src/domains/gamification/components/GamificationScreen/styles.ts +43 -0
- package/src/domains/gamification/components/GamificationScreen/types.ts +77 -0
- package/src/domains/gamification/components/GamificationScreenWrapper.tsx +91 -0
- package/src/domains/gamification/components/GamificationSettingsItem.tsx +33 -0
- package/src/domains/gamification/components/LevelProgress.tsx +129 -0
- package/src/domains/gamification/components/PointsBadge.tsx +60 -0
- package/src/domains/gamification/components/StatsCard.tsx +89 -0
- package/src/domains/gamification/components/StreakDisplay.tsx +119 -0
- package/src/domains/gamification/components/index.ts +13 -0
- package/src/domains/gamification/examples/gamification.config.example.ts +70 -0
- package/src/domains/gamification/examples/localization.example.json +71 -0
- package/src/domains/gamification/hooks/useGamification.ts +91 -0
- package/src/domains/gamification/index.ts +65 -0
- package/src/domains/gamification/store/gamificationStore.ts +162 -0
- package/src/domains/gamification/types/index.ts +103 -0
- package/src/domains/gamification/types/settings.ts +28 -0
- package/src/domains/gamification/utils/calculations.ts +85 -0
- package/src/index.ts +18 -8
- package/src/presentation/navigation/SettingsStackNavigator.tsx +12 -0
- package/src/presentation/navigation/types.ts +2 -0
- package/src/presentation/navigation/utils/navigationScreenOptions.ts +7 -0
- package/src/presentation/screens/types/UserFeatureConfig.ts +2 -0
- package/src/presentation/utils/configCreators.ts +147 -0
- package/src/presentation/utils/index.ts +5 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -51
- package/.github/ISSUE_TEMPLATE/documentation.md +0 -52
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -63
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -84
- package/AI_AGENT_GUIDELINES.md +0 -367
- package/ARCHITECTURE.md +0 -246
- package/CHANGELOG.md +0 -67
- package/CODE_OF_CONDUCT.md +0 -75
- package/CONTRIBUTING.md +0 -107
- package/DOCUMENTATION_MIGRATION.md +0 -319
- package/DOCUMENTATION_TEMPLATE.md +0 -155
- package/SECURITY.md +0 -98
- package/SETTINGS_SCREEN_GUIDE.md +0 -185
- package/TESTING.md +0 -358
- package/src/__tests__/integration.test.tsx +0 -371
- package/src/__tests__/performance.test.tsx +0 -369
- package/src/__tests__/setup.test.tsx +0 -20
- package/src/__tests__/setup.ts +0 -154
- package/src/domains/about/__tests__/integration.test.tsx +0 -328
- package/src/domains/about/__tests__/types.d.ts +0 -5
- package/src/domains/about/domain/entities/__tests__/AppInfo.test.ts +0 -93
- package/src/domains/about/infrastructure/repositories/__tests__/AboutRepository.test.ts +0 -153
- package/src/domains/about/presentation/components/__tests__/AboutContent.simple.test.tsx +0 -178
- package/src/domains/about/presentation/components/__tests__/AboutContent.test.tsx +0 -293
- package/src/domains/about/presentation/components/__tests__/AboutHeader.test.tsx +0 -201
- package/src/domains/about/presentation/components/__tests__/AboutSettingItem.test.tsx +0 -71
- package/src/domains/about/presentation/hooks/__tests__/useAboutInfo.simple.test.tsx +0 -229
- package/src/domains/about/presentation/hooks/__tests__/useAboutInfo.test.tsx +0 -240
- package/src/domains/about/presentation/screens/__tests__/AboutScreen.simple.test.tsx +0 -199
- package/src/domains/about/presentation/screens/__tests__/AboutScreen.test.tsx +0 -366
- package/src/domains/about/utils/__tests__/index.test.ts +0 -408
- package/src/domains/appearance/__tests__/components/AppearanceScreen.test.tsx +0 -195
- package/src/domains/appearance/__tests__/hooks/index.test.tsx +0 -232
- package/src/domains/appearance/__tests__/integration/index.test.tsx +0 -207
- package/src/domains/appearance/__tests__/services/appearanceService.test.ts +0 -299
- package/src/domains/appearance/__tests__/setup.ts +0 -88
- package/src/domains/appearance/__tests__/stores/appearanceStore.test.tsx +0 -175
- package/src/domains/cloud-sync/presentation/components/__tests__/CloudSyncSetting.test.tsx +0 -78
- package/src/domains/legal/__tests__/ContentValidationService.test.ts +0 -195
- package/src/domains/legal/__tests__/StyleCacheService.test.ts +0 -110
- package/src/domains/legal/__tests__/UrlHandlerService.test.ts +0 -71
- package/src/domains/legal/__tests__/setup.ts +0 -82
- package/src/presentation/components/__tests__/SettingsErrorBoundary.test.tsx +0 -186
- package/src/presentation/screens/__tests__/SettingsScreen.test.tsx +0 -322
- package/src/presentation/screens/hooks/__tests__/useFeatureDetection.test.tsx +0 -261
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useGamification Hook
|
|
3
|
+
* Main hook for gamification operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useCallback, useEffect, useMemo } from "react";
|
|
7
|
+
import { useGamificationStore } from "../store/gamificationStore";
|
|
8
|
+
import { calculateLevel } from "../utils/calculations";
|
|
9
|
+
import type { GamificationConfig, LevelState, Achievement } from "../types";
|
|
10
|
+
|
|
11
|
+
export interface UseGamificationReturn {
|
|
12
|
+
// State
|
|
13
|
+
points: number;
|
|
14
|
+
totalTasksCompleted: number;
|
|
15
|
+
level: LevelState;
|
|
16
|
+
streak: { current: number; longest: number };
|
|
17
|
+
achievements: Achievement[];
|
|
18
|
+
isLoading: boolean;
|
|
19
|
+
isInitialized: boolean;
|
|
20
|
+
|
|
21
|
+
// Actions
|
|
22
|
+
initialize: (config: GamificationConfig) => Promise<void>;
|
|
23
|
+
completeTask: () => void;
|
|
24
|
+
addPoints: (amount: number) => void;
|
|
25
|
+
reset: () => Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const useGamification = (
|
|
29
|
+
config?: GamificationConfig
|
|
30
|
+
): UseGamificationReturn => {
|
|
31
|
+
const store = useGamificationStore();
|
|
32
|
+
|
|
33
|
+
// Auto-initialize if config provided
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (config && !store.isInitialized) {
|
|
36
|
+
store.initialize(config);
|
|
37
|
+
}
|
|
38
|
+
}, [config, store.isInitialized]);
|
|
39
|
+
|
|
40
|
+
// Calculate level from config
|
|
41
|
+
const level = useMemo((): LevelState => {
|
|
42
|
+
if (!config?.levels.length) {
|
|
43
|
+
return {
|
|
44
|
+
currentLevel: 1,
|
|
45
|
+
currentPoints: store.points,
|
|
46
|
+
pointsToNext: 50,
|
|
47
|
+
progress: Math.min(100, (store.points / 50) * 100),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return calculateLevel(store.points, config.levels);
|
|
51
|
+
}, [store.points, config?.levels]);
|
|
52
|
+
|
|
53
|
+
const completeTask = useCallback(() => {
|
|
54
|
+
store.completeTask();
|
|
55
|
+
}, [store.completeTask]);
|
|
56
|
+
|
|
57
|
+
const addPoints = useCallback(
|
|
58
|
+
(amount: number) => {
|
|
59
|
+
store.addPoints(amount);
|
|
60
|
+
},
|
|
61
|
+
[store.addPoints]
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const initialize = useCallback(
|
|
65
|
+
async (cfg: GamificationConfig) => {
|
|
66
|
+
await store.initialize(cfg);
|
|
67
|
+
},
|
|
68
|
+
[store.initialize]
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const reset = useCallback(async () => {
|
|
72
|
+
await store.reset();
|
|
73
|
+
}, [store.reset]);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
points: store.points,
|
|
77
|
+
totalTasksCompleted: store.totalTasksCompleted,
|
|
78
|
+
level,
|
|
79
|
+
streak: {
|
|
80
|
+
current: store.streak.current,
|
|
81
|
+
longest: store.streak.longest,
|
|
82
|
+
},
|
|
83
|
+
achievements: store.achievements,
|
|
84
|
+
isLoading: store.isLoading,
|
|
85
|
+
isInitialized: store.isInitialized,
|
|
86
|
+
initialize,
|
|
87
|
+
completeTask,
|
|
88
|
+
addPoints,
|
|
89
|
+
reset,
|
|
90
|
+
};
|
|
91
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @umituz/react-native-gamification
|
|
3
|
+
*
|
|
4
|
+
* Generic gamification system for React Native apps
|
|
5
|
+
* All text via props - NO hardcoded strings
|
|
6
|
+
* Designed for 100+ apps
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Types
|
|
10
|
+
export type {
|
|
11
|
+
AchievementDefinition,
|
|
12
|
+
Achievement,
|
|
13
|
+
LevelDefinition,
|
|
14
|
+
LevelState,
|
|
15
|
+
StreakState,
|
|
16
|
+
GamificationConfig,
|
|
17
|
+
GamificationState,
|
|
18
|
+
GamificationActions,
|
|
19
|
+
GamificationStore,
|
|
20
|
+
} from "./types";
|
|
21
|
+
|
|
22
|
+
// Settings Integration Types
|
|
23
|
+
export type {
|
|
24
|
+
GamificationSettingsConfig,
|
|
25
|
+
GamificationMenuConfig,
|
|
26
|
+
} from "./types/settings";
|
|
27
|
+
|
|
28
|
+
// Store
|
|
29
|
+
export { useGamificationStore } from "./store/gamificationStore";
|
|
30
|
+
|
|
31
|
+
// Hooks
|
|
32
|
+
export { useGamification, type UseGamificationReturn } from "./hooks/useGamification";
|
|
33
|
+
|
|
34
|
+
// Utils
|
|
35
|
+
export {
|
|
36
|
+
calculateLevel,
|
|
37
|
+
checkAchievementUnlock,
|
|
38
|
+
updateAchievementProgress,
|
|
39
|
+
isStreakActive,
|
|
40
|
+
isSameDay,
|
|
41
|
+
} from "./utils/calculations";
|
|
42
|
+
|
|
43
|
+
// Components
|
|
44
|
+
export {
|
|
45
|
+
LevelProgress,
|
|
46
|
+
type LevelProgressProps,
|
|
47
|
+
PointsBadge,
|
|
48
|
+
type PointsBadgeProps,
|
|
49
|
+
AchievementCard,
|
|
50
|
+
type AchievementCardProps,
|
|
51
|
+
AchievementToast,
|
|
52
|
+
type AchievementToastProps,
|
|
53
|
+
StreakDisplay,
|
|
54
|
+
type StreakDisplayProps,
|
|
55
|
+
StatsCard,
|
|
56
|
+
type StatsCardProps,
|
|
57
|
+
AchievementItem,
|
|
58
|
+
type AchievementItemProps,
|
|
59
|
+
GamificationScreen,
|
|
60
|
+
type GamificationScreenProps,
|
|
61
|
+
} from "./components";
|
|
62
|
+
|
|
63
|
+
// Settings Integration Components
|
|
64
|
+
export { GamificationScreenWrapper } from "./components/GamificationScreenWrapper";
|
|
65
|
+
export { GamificationSettingsItem } from "./components/GamificationSettingsItem";
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gamification Store
|
|
3
|
+
* Zustand store with persist middleware
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createStore } from "@umituz/react-native-storage";
|
|
7
|
+
import type {
|
|
8
|
+
GamificationState,
|
|
9
|
+
GamificationActions,
|
|
10
|
+
GamificationConfig,
|
|
11
|
+
Achievement,
|
|
12
|
+
} from "../types";
|
|
13
|
+
import {
|
|
14
|
+
checkAchievementUnlock,
|
|
15
|
+
updateAchievementProgress,
|
|
16
|
+
isStreakActive,
|
|
17
|
+
isSameDay,
|
|
18
|
+
} from "../utils/calculations";
|
|
19
|
+
|
|
20
|
+
const DEFAULT_STATE: GamificationState = {
|
|
21
|
+
points: 0,
|
|
22
|
+
totalTasksCompleted: 0,
|
|
23
|
+
achievements: [],
|
|
24
|
+
streak: {
|
|
25
|
+
current: 0,
|
|
26
|
+
longest: 0,
|
|
27
|
+
lastActivityDate: null,
|
|
28
|
+
},
|
|
29
|
+
isLoading: false,
|
|
30
|
+
isInitialized: false,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
let currentConfig: GamificationConfig | null = null;
|
|
34
|
+
|
|
35
|
+
export const useGamificationStore = createStore<GamificationState, GamificationActions>({
|
|
36
|
+
name: "gamification-storage",
|
|
37
|
+
initialState: DEFAULT_STATE,
|
|
38
|
+
persist: true,
|
|
39
|
+
version: 1,
|
|
40
|
+
partialize: (state) => ({
|
|
41
|
+
points: state.points,
|
|
42
|
+
totalTasksCompleted: state.totalTasksCompleted,
|
|
43
|
+
achievements: state.achievements,
|
|
44
|
+
streak: state.streak,
|
|
45
|
+
isLoading: false,
|
|
46
|
+
isInitialized: false,
|
|
47
|
+
}),
|
|
48
|
+
actions: (set, get) => ({
|
|
49
|
+
initialize: async (config: GamificationConfig) => {
|
|
50
|
+
currentConfig = config;
|
|
51
|
+
const state = get();
|
|
52
|
+
|
|
53
|
+
// Initialize achievements from config
|
|
54
|
+
const achievements: Achievement[] = config.achievements.map((def) => ({
|
|
55
|
+
...def,
|
|
56
|
+
isUnlocked: false,
|
|
57
|
+
progress: 0,
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
// Merge with existing unlocked achievements
|
|
61
|
+
const mergedAchievements = achievements.map((ach: Achievement) => {
|
|
62
|
+
const existing = state.achievements.find((a: Achievement) => a.id === ach.id);
|
|
63
|
+
if (existing) {
|
|
64
|
+
return { ...ach, isUnlocked: existing.isUnlocked, progress: existing.progress };
|
|
65
|
+
}
|
|
66
|
+
return ach;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
set({ achievements: mergedAchievements, isInitialized: true });
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
addPoints: (amount: number) => {
|
|
73
|
+
const state = get();
|
|
74
|
+
set({ points: state.points + amount });
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
completeTask: () => {
|
|
78
|
+
const state = get();
|
|
79
|
+
const pointsToAdd = currentConfig?.pointsPerAction ?? 15;
|
|
80
|
+
|
|
81
|
+
set({
|
|
82
|
+
totalTasksCompleted: state.totalTasksCompleted + 1,
|
|
83
|
+
points: state.points + pointsToAdd,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Update streak
|
|
87
|
+
get().updateStreak();
|
|
88
|
+
|
|
89
|
+
// Check achievements
|
|
90
|
+
get().checkAchievements();
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
updateStreak: () => {
|
|
94
|
+
const state = get();
|
|
95
|
+
const now = new Date();
|
|
96
|
+
const lastDate = state.streak.lastActivityDate
|
|
97
|
+
? new Date(state.streak.lastActivityDate)
|
|
98
|
+
: null;
|
|
99
|
+
|
|
100
|
+
let newStreak = state.streak.current;
|
|
101
|
+
|
|
102
|
+
if (!lastDate || !isSameDay(lastDate, now)) {
|
|
103
|
+
if (isStreakActive(state.streak.lastActivityDate)) {
|
|
104
|
+
newStreak = state.streak.current + 1;
|
|
105
|
+
} else {
|
|
106
|
+
newStreak = 1;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
set({
|
|
111
|
+
streak: {
|
|
112
|
+
current: newStreak,
|
|
113
|
+
longest: Math.max(state.streak.longest, newStreak),
|
|
114
|
+
lastActivityDate: now.toISOString(),
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
checkAchievements: () => {
|
|
120
|
+
if (!currentConfig) return [];
|
|
121
|
+
|
|
122
|
+
const state = get();
|
|
123
|
+
const newlyUnlocked: Achievement[] = [];
|
|
124
|
+
|
|
125
|
+
const updatedAchievements = state.achievements.map((ach: Achievement) => {
|
|
126
|
+
if (ach.isUnlocked) return ach;
|
|
127
|
+
|
|
128
|
+
const progress = updateAchievementProgress(
|
|
129
|
+
ach,
|
|
130
|
+
state.totalTasksCompleted,
|
|
131
|
+
state.streak.current
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const shouldUnlock = checkAchievementUnlock(
|
|
135
|
+
ach,
|
|
136
|
+
state.totalTasksCompleted,
|
|
137
|
+
state.streak.current
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
if (shouldUnlock && !ach.isUnlocked) {
|
|
141
|
+
const unlocked = {
|
|
142
|
+
...ach,
|
|
143
|
+
isUnlocked: true,
|
|
144
|
+
unlockedAt: new Date().toISOString(),
|
|
145
|
+
progress: 100,
|
|
146
|
+
};
|
|
147
|
+
newlyUnlocked.push(unlocked);
|
|
148
|
+
return unlocked;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return { ...ach, progress };
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
set({ achievements: updatedAchievements });
|
|
155
|
+
return newlyUnlocked;
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
reset: async () => {
|
|
159
|
+
set(DEFAULT_STATE);
|
|
160
|
+
},
|
|
161
|
+
}),
|
|
162
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gamification Types
|
|
3
|
+
* Generic types for 100+ apps - NO app-specific code
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Achievement Definition (provided by app)
|
|
7
|
+
export interface AchievementDefinition {
|
|
8
|
+
id: string;
|
|
9
|
+
threshold: number;
|
|
10
|
+
type: "count" | "streak" | "milestone";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Achievement State (internal)
|
|
14
|
+
export interface Achievement extends AchievementDefinition {
|
|
15
|
+
isUnlocked: boolean;
|
|
16
|
+
unlockedAt?: string;
|
|
17
|
+
progress: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Level Definition (provided by app)
|
|
21
|
+
export interface LevelDefinition {
|
|
22
|
+
level: number;
|
|
23
|
+
minPoints: number;
|
|
24
|
+
maxPoints: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Level State (internal)
|
|
28
|
+
export interface LevelState {
|
|
29
|
+
currentLevel: number;
|
|
30
|
+
currentPoints: number;
|
|
31
|
+
pointsToNext: number;
|
|
32
|
+
progress: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Streak State
|
|
36
|
+
export interface StreakState {
|
|
37
|
+
current: number;
|
|
38
|
+
longest: number;
|
|
39
|
+
lastActivityDate: string | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Gamification Config (provided by app via props)
|
|
43
|
+
export interface GamificationConfig {
|
|
44
|
+
storageKey: string;
|
|
45
|
+
achievements: AchievementDefinition[];
|
|
46
|
+
levels: LevelDefinition[];
|
|
47
|
+
pointsPerAction?: number;
|
|
48
|
+
streakBonusMultiplier?: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Store State
|
|
52
|
+
export interface GamificationState {
|
|
53
|
+
points: number;
|
|
54
|
+
totalTasksCompleted: number;
|
|
55
|
+
achievements: Achievement[];
|
|
56
|
+
streak: StreakState;
|
|
57
|
+
isLoading: boolean;
|
|
58
|
+
isInitialized: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Store Actions
|
|
62
|
+
export interface GamificationActions {
|
|
63
|
+
initialize: (config: GamificationConfig) => Promise<void>;
|
|
64
|
+
addPoints: (amount: number) => void;
|
|
65
|
+
completeTask: () => void;
|
|
66
|
+
updateStreak: () => void;
|
|
67
|
+
checkAchievements: () => Achievement[];
|
|
68
|
+
reset: () => Promise<void>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Combined Store
|
|
72
|
+
export type GamificationStore = GamificationState & GamificationActions;
|
|
73
|
+
|
|
74
|
+
// UI Component Props (all text via props - NO hardcoded strings)
|
|
75
|
+
export interface LevelProgressProps {
|
|
76
|
+
level: number;
|
|
77
|
+
points: number;
|
|
78
|
+
levelTitle: string;
|
|
79
|
+
pointsToNext: number;
|
|
80
|
+
progress: number;
|
|
81
|
+
showPoints?: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface AchievementCardProps {
|
|
85
|
+
title: string;
|
|
86
|
+
description: string;
|
|
87
|
+
icon: string;
|
|
88
|
+
isUnlocked: boolean;
|
|
89
|
+
progress: number;
|
|
90
|
+
threshold: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface StreakDisplayProps {
|
|
94
|
+
current: number;
|
|
95
|
+
longest: number;
|
|
96
|
+
streakLabel: string;
|
|
97
|
+
bestLabel: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface PointsBadgeProps {
|
|
101
|
+
points: number;
|
|
102
|
+
label?: string;
|
|
103
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gamification Settings Integration Types
|
|
3
|
+
* Wrapper types for integrating gamification into settings
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { GamificationConfig } from "./index";
|
|
7
|
+
import type { GamificationScreenProps } from "../components/GamificationScreen/types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Configuration for gamification in settings
|
|
11
|
+
*/
|
|
12
|
+
export interface GamificationSettingsConfig {
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
config: GamificationConfig;
|
|
15
|
+
screenProps: Omit<GamificationScreenProps, "levelProps" | "stats" | "achievements">;
|
|
16
|
+
onNavigate?: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Configuration for gamification menu item
|
|
21
|
+
*/
|
|
22
|
+
export interface GamificationMenuConfig {
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
title: string;
|
|
25
|
+
subtitle?: string;
|
|
26
|
+
icon?: string;
|
|
27
|
+
onPress?: () => void;
|
|
28
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gamification Calculations
|
|
3
|
+
* Pure utility functions - NO side effects
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { LevelDefinition, LevelState, Achievement, AchievementDefinition } from "../types";
|
|
7
|
+
|
|
8
|
+
export const calculateLevel = (
|
|
9
|
+
points: number,
|
|
10
|
+
levels: LevelDefinition[]
|
|
11
|
+
): LevelState => {
|
|
12
|
+
const sortedLevels = [...levels].sort((a, b) => a.minPoints - b.minPoints);
|
|
13
|
+
|
|
14
|
+
let currentLevelDef = sortedLevels[0];
|
|
15
|
+
let nextLevelDef: LevelDefinition | null = null;
|
|
16
|
+
|
|
17
|
+
for (let i = 0; i < sortedLevels.length; i++) {
|
|
18
|
+
if (points >= sortedLevels[i].minPoints) {
|
|
19
|
+
currentLevelDef = sortedLevels[i];
|
|
20
|
+
nextLevelDef = sortedLevels[i + 1] || null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const pointsInLevel = points - currentLevelDef.minPoints;
|
|
25
|
+
const levelRange = nextLevelDef
|
|
26
|
+
? nextLevelDef.minPoints - currentLevelDef.minPoints
|
|
27
|
+
: 100;
|
|
28
|
+
const progress = Math.min(100, (pointsInLevel / levelRange) * 100);
|
|
29
|
+
const pointsToNext = nextLevelDef
|
|
30
|
+
? nextLevelDef.minPoints - points
|
|
31
|
+
: 0;
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
currentLevel: currentLevelDef.level,
|
|
35
|
+
currentPoints: points,
|
|
36
|
+
pointsToNext,
|
|
37
|
+
progress,
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const checkAchievementUnlock = (
|
|
42
|
+
definition: AchievementDefinition,
|
|
43
|
+
tasksCompleted: number,
|
|
44
|
+
currentStreak: number
|
|
45
|
+
): boolean => {
|
|
46
|
+
switch (definition.type) {
|
|
47
|
+
case "count":
|
|
48
|
+
return tasksCompleted >= definition.threshold;
|
|
49
|
+
case "streak":
|
|
50
|
+
return currentStreak >= definition.threshold;
|
|
51
|
+
case "milestone":
|
|
52
|
+
return tasksCompleted >= definition.threshold;
|
|
53
|
+
default:
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const updateAchievementProgress = (
|
|
59
|
+
definition: AchievementDefinition,
|
|
60
|
+
tasksCompleted: number,
|
|
61
|
+
currentStreak: number
|
|
62
|
+
): number => {
|
|
63
|
+
const value = definition.type === "streak" ? currentStreak : tasksCompleted;
|
|
64
|
+
return Math.min(100, (value / definition.threshold) * 100);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const isStreakActive = (lastActivityDate: string | null): boolean => {
|
|
68
|
+
if (!lastActivityDate) return false;
|
|
69
|
+
|
|
70
|
+
const last = new Date(lastActivityDate);
|
|
71
|
+
const now = new Date();
|
|
72
|
+
const diffDays = Math.floor(
|
|
73
|
+
(now.getTime() - last.getTime()) / (1000 * 60 * 60 * 24)
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
return diffDays <= 1;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const isSameDay = (date1: Date, date2: Date): boolean => {
|
|
80
|
+
return (
|
|
81
|
+
date1.getFullYear() === date2.getFullYear() &&
|
|
82
|
+
date1.getMonth() === date2.getMonth() &&
|
|
83
|
+
date1.getDate() === date2.getDate()
|
|
84
|
+
);
|
|
85
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -120,14 +120,24 @@ export * from "./domains/cloud-sync";
|
|
|
120
120
|
// Dev Domain - Development-only settings (DEV mode)
|
|
121
121
|
export * from "./domains/dev";
|
|
122
122
|
|
|
123
|
+
// Gamification Domain - Achievements, levels, streaks
|
|
124
|
+
export * from "./domains/gamification";
|
|
125
|
+
|
|
126
|
+
|
|
123
127
|
// =============================================================================
|
|
124
|
-
// PRESENTATION LAYER -
|
|
128
|
+
// PRESENTATION LAYER - Config Creator Utilities
|
|
125
129
|
// =============================================================================
|
|
126
130
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
export {
|
|
132
|
+
createAppearanceConfig,
|
|
133
|
+
createLanguageConfig,
|
|
134
|
+
createNotificationsConfig,
|
|
135
|
+
createAboutConfig,
|
|
136
|
+
createLegalConfig,
|
|
137
|
+
createFeedbackConfig,
|
|
138
|
+
createRatingConfig,
|
|
139
|
+
createFAQConfig,
|
|
140
|
+
createSubscriptionConfig,
|
|
141
|
+
type TranslationFunction,
|
|
142
|
+
type FeedbackFormData,
|
|
143
|
+
} from './presentation/utils';
|
|
@@ -29,8 +29,10 @@ import {
|
|
|
29
29
|
createNotificationsScreenOptions,
|
|
30
30
|
createFAQScreenOptions,
|
|
31
31
|
createLanguageSelectionScreenOptions,
|
|
32
|
+
createGamificationScreenOptions,
|
|
32
33
|
} from "./utils";
|
|
33
34
|
import type { SettingsStackParamList, SettingsStackNavigatorProps } from "./types";
|
|
35
|
+
import { GamificationScreenWrapper } from "../../domains/gamification";
|
|
34
36
|
|
|
35
37
|
const Stack = createStackNavigator<SettingsStackParamList>();
|
|
36
38
|
|
|
@@ -45,6 +47,7 @@ export const SettingsStackNavigator: React.FC<SettingsStackNavigatorProps> = ({
|
|
|
45
47
|
devSettings,
|
|
46
48
|
customSections = [],
|
|
47
49
|
showHeader = true,
|
|
50
|
+
gamificationConfig,
|
|
48
51
|
}) => {
|
|
49
52
|
const tokens = useAppDesignTokens();
|
|
50
53
|
const { t } = useLocalization();
|
|
@@ -143,6 +146,15 @@ export const SettingsStackNavigator: React.FC<SettingsStackNavigatorProps> = ({
|
|
|
143
146
|
) : null
|
|
144
147
|
)}
|
|
145
148
|
|
|
149
|
+
{gamificationConfig?.enabled && (
|
|
150
|
+
<Stack.Screen
|
|
151
|
+
name="Gamification"
|
|
152
|
+
options={createGamificationScreenOptions(t)}
|
|
153
|
+
>
|
|
154
|
+
{() => <GamificationScreenWrapper config={gamificationConfig} />}
|
|
155
|
+
</Stack.Screen>
|
|
156
|
+
)}
|
|
157
|
+
|
|
146
158
|
<Stack.Screen
|
|
147
159
|
name="LanguageSelection"
|
|
148
160
|
options={createLanguageSelectionScreenOptions(t)}
|
|
@@ -42,6 +42,7 @@ export type SettingsStackParamList = {
|
|
|
42
42
|
Notifications: undefined;
|
|
43
43
|
FAQ: undefined;
|
|
44
44
|
LanguageSelection: undefined;
|
|
45
|
+
Gamification: undefined;
|
|
45
46
|
};
|
|
46
47
|
|
|
47
48
|
/**
|
|
@@ -89,4 +90,5 @@ export interface SettingsStackNavigatorProps {
|
|
|
89
90
|
devSettings?: DevSettingsProps;
|
|
90
91
|
customSections?: CustomSettingsSection[];
|
|
91
92
|
showHeader?: boolean;
|
|
93
|
+
gamificationConfig?: import("../../domains/gamification").GamificationSettingsConfig;
|
|
92
94
|
}
|
|
@@ -54,3 +54,10 @@ export const createLanguageSelectionScreenOptions = (t: any) => ({
|
|
|
54
54
|
headerTitle: t("settings.language.title"),
|
|
55
55
|
headerTitleAlign: "center" as const,
|
|
56
56
|
});
|
|
57
|
+
|
|
58
|
+
export const createGamificationScreenOptions = (t: any) => ({
|
|
59
|
+
headerShown: true,
|
|
60
|
+
headerTitle: t("settings.gamification.title"),
|
|
61
|
+
headerTitleAlign: "center" as const,
|
|
62
|
+
});
|
|
63
|
+
|
|
@@ -12,6 +12,8 @@ import type { FeedbackType } from "../../../domains/feedback/domain/entities/Fee
|
|
|
12
12
|
export interface UserProfileConfig {
|
|
13
13
|
/** Show user profile header */
|
|
14
14
|
enabled?: boolean;
|
|
15
|
+
/** Custom section title for grouping */
|
|
16
|
+
sectionTitle?: string;
|
|
15
17
|
/** Custom display name for anonymous users */
|
|
16
18
|
anonymousDisplayName?: string;
|
|
17
19
|
/** Custom avatar service URL */
|