@phygitallabs/tapquest-core 6.6.0 → 6.7.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/bun.lock +9 -9
- package/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +20 -5
- package/dist/index.d.ts +20 -5
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +4 -1
- package/src/modules/achievement/helpers/index.ts +84 -68
- package/src/modules/achievement/helpers/useClearAchievementProgressCache.tsx +16 -20
- package/src/modules/achievement-tracking/hooks/index.ts +1 -0
- package/src/modules/achievement-tracking/index.ts +3 -0
- package/src/modules/achievement-tracking/providers/AchievementTrackingProvider.tsx +138 -0
- package/src/modules/achievement-tracking/providers/index.ts +1 -0
- package/src/modules/achievement-tracking/types/index.ts +21 -0
- package/src/modules/action-logs/hooks/index.ts +3 -0
- package/src/modules/action-logs/index.ts +1 -0
- package/tsup.config.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phygitallabs/tapquest-core",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.7.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@openreplay/tracker": "^16.4.10",
|
|
27
27
|
"@phygitallabs/achievement": "latest",
|
|
28
|
-
"@phygitallabs/api-core": "
|
|
28
|
+
"@phygitallabs/api-core": "^6.3.0",
|
|
29
29
|
"@phygitallabs/authentication": "latest",
|
|
30
30
|
"@phygitallabs/generate-certificate": "latest",
|
|
31
31
|
"@phygitallabs/helpers": "latest",
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Export all modules
|
|
2
2
|
export * from "./modules/achievement";
|
|
3
|
+
export * from "./modules/achievement-tracking";
|
|
3
4
|
export * from "./modules/reward";
|
|
4
5
|
export * from "./modules/notification";
|
|
5
6
|
export * from "./modules/memory";
|
|
@@ -24,8 +25,10 @@ export * from "./modules/achivementWithReward";
|
|
|
24
25
|
|
|
25
26
|
export * from "./modules/send-email";
|
|
26
27
|
|
|
28
|
+
export * from "./modules/session-replay";
|
|
29
|
+
|
|
27
30
|
export * from "./helper";
|
|
28
31
|
|
|
29
32
|
export * from "./modules/session-replay";
|
|
30
33
|
|
|
31
|
-
export
|
|
34
|
+
export * from "./modules/action-logs";
|
|
@@ -1,46 +1,70 @@
|
|
|
1
1
|
import { Achievement, UserAchievementProgress } from "../types";
|
|
2
2
|
|
|
3
3
|
const getLocationIdsFromAchievementRule = (achievement: Achievement) => {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
});
|
|
21
|
-
});
|
|
4
|
+
if (!achievement.rule) return [];
|
|
5
|
+
const locationIds: string[] = [];
|
|
6
|
+
Object.values(achievement.rule).forEach((ruleList) => {
|
|
7
|
+
if (!ruleList.rules) return;
|
|
8
|
+
ruleList.rules.forEach((rule) => {
|
|
9
|
+
if (!rule.filter) return;
|
|
10
|
+
Object.values(rule.filter).forEach((filterList) => {
|
|
11
|
+
if (!filterList.filters) return;
|
|
12
|
+
filterList.filters.forEach((filter) => {
|
|
13
|
+
if (filter.label === "location_id" && filter.value) {
|
|
14
|
+
if (Array.isArray(filter.value)) {
|
|
15
|
+
locationIds.push(...filter.value);
|
|
16
|
+
} else {
|
|
17
|
+
locationIds.push(filter.value);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
22
20
|
});
|
|
21
|
+
});
|
|
23
22
|
});
|
|
24
|
-
|
|
23
|
+
});
|
|
24
|
+
return Array.from(new Set(locationIds)) as string[];
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
const getSurveyIdsFromAchievementRule = (achievement: Achievement) => {
|
|
28
|
+
if (!achievement.rule) return [];
|
|
29
|
+
const surveyIds: string[] = [];
|
|
30
|
+
Object.values(achievement.rule).forEach((ruleList) => {
|
|
31
|
+
if (!ruleList.rules) return;
|
|
32
|
+
ruleList.rules.forEach((rule) => {
|
|
33
|
+
if (!rule.filter) return;
|
|
34
|
+
Object.values(rule.filter).forEach((filterList) => {
|
|
35
|
+
if (!filterList.filters) return;
|
|
36
|
+
filterList.filters.forEach((filter) => {
|
|
37
|
+
if (filter.label === "survey_id" && filter.value) {
|
|
38
|
+
if (Array.isArray(filter.value)) {
|
|
39
|
+
surveyIds.push(...filter.value);
|
|
40
|
+
} else {
|
|
41
|
+
surveyIds.push(filter.value);
|
|
35
42
|
}
|
|
43
|
+
}
|
|
36
44
|
});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
return Array.from(new Set(surveyIds)) as string[];
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const getActionsFromAchievementRule = (achievement: Achievement) => {
|
|
52
|
+
if (!achievement?.rule) return [];
|
|
53
|
+
const actions: string[] = [];
|
|
54
|
+
Object.values(achievement?.rule || {}).forEach((ruleList) => {
|
|
55
|
+
if (!ruleList?.rules) return;
|
|
56
|
+
ruleList?.rules?.forEach((rule) => {
|
|
57
|
+
if (rule?.action) {
|
|
58
|
+
actions.push(rule?.action);
|
|
59
|
+
}
|
|
37
60
|
});
|
|
38
|
-
|
|
61
|
+
});
|
|
62
|
+
return Array.from(new Set(actions)) as string[];
|
|
39
63
|
};
|
|
40
64
|
|
|
41
65
|
const isAchievementCompleted = (achievement: UserAchievementProgress) => {
|
|
42
|
-
|
|
43
|
-
}
|
|
66
|
+
return achievement.isCompleted || achievement.overallPercentage === 100;
|
|
67
|
+
};
|
|
44
68
|
|
|
45
69
|
type SnakeToCamelCase<S extends string> = S extends `${infer T}_${infer U}`
|
|
46
70
|
? `${T}${Capitalize<SnakeToCamelCase<U>>}`
|
|
@@ -49,52 +73,44 @@ type SnakeToCamelCase<S extends string> = S extends `${infer T}_${infer U}`
|
|
|
49
73
|
type ConvertSnakeToCamel<T> = T extends (infer U)[]
|
|
50
74
|
? ConvertSnakeToCamel<U>[]
|
|
51
75
|
: T extends Record<string, unknown>
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
: T;
|
|
76
|
+
? {
|
|
77
|
+
[K in keyof T as K extends string ? SnakeToCamelCase<K> : K]: ConvertSnakeToCamel<T[K]>;
|
|
78
|
+
}
|
|
79
|
+
: T;
|
|
58
80
|
|
|
59
81
|
/**
|
|
60
82
|
* Converts snake_case keys to camelCase keys in an object or array of objects
|
|
61
83
|
*/
|
|
62
84
|
export function convertSnakeToCamel<T>(obj: T): ConvertSnakeToCamel<T> {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const camelKey = key.replace(/_([a-z])/g, (_, letter) =>
|
|
79
|
-
letter.toUpperCase()
|
|
80
|
-
);
|
|
81
|
-
converted[camelKey] = convertSnakeToCamel(
|
|
82
|
-
(obj as Record<string, unknown>)[key]
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
+
if (obj === null || obj === undefined) {
|
|
86
|
+
return obj as ConvertSnakeToCamel<T>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (Array.isArray(obj)) {
|
|
90
|
+
return obj.map((item) => convertSnakeToCamel(item)) as ConvertSnakeToCamel<T>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (typeof obj === "object" && obj.constructor === Object) {
|
|
94
|
+
const converted: Record<string, unknown> = {};
|
|
95
|
+
|
|
96
|
+
for (const key in obj) {
|
|
97
|
+
if (obj.hasOwnProperty(key)) {
|
|
98
|
+
const camelKey = key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
99
|
+
converted[camelKey] = convertSnakeToCamel((obj as Record<string, unknown>)[key]);
|
|
85
100
|
}
|
|
86
|
-
|
|
87
|
-
return converted as ConvertSnakeToCamel<T>;
|
|
88
101
|
}
|
|
89
|
-
|
|
90
|
-
return
|
|
102
|
+
|
|
103
|
+
return converted as ConvertSnakeToCamel<T>;
|
|
91
104
|
}
|
|
92
|
-
|
|
93
105
|
|
|
94
|
-
|
|
95
|
-
getLocationIdsFromAchievementRule,
|
|
96
|
-
getActionsFromAchievementRule,
|
|
97
|
-
isAchievementCompleted
|
|
106
|
+
return obj as ConvertSnakeToCamel<T>;
|
|
98
107
|
}
|
|
99
108
|
|
|
100
|
-
export {
|
|
109
|
+
export {
|
|
110
|
+
getLocationIdsFromAchievementRule,
|
|
111
|
+
getActionsFromAchievementRule,
|
|
112
|
+
getSurveyIdsFromAchievementRule,
|
|
113
|
+
isAchievementCompleted,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export { useClearAchievementProgressCache } from "./useClearAchievementProgressCache";
|
|
@@ -5,29 +5,25 @@ export function useClearAchievementProgressCache() {
|
|
|
5
5
|
const queryClient = useQueryClient();
|
|
6
6
|
|
|
7
7
|
const clearCache = useCallback(
|
|
8
|
-
(options?: {
|
|
9
|
-
refetchActive?: boolean;
|
|
10
|
-
refetchInactive?: boolean;
|
|
11
|
-
}) => {
|
|
8
|
+
(options?: { refetchActive?: boolean; refetchInactive?: boolean }) => {
|
|
12
9
|
const { refetchActive = true, refetchInactive = false } = options || {};
|
|
13
10
|
|
|
14
|
-
// Clear all achievement progress related queries
|
|
15
|
-
// Query keys from packages
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
];
|
|
11
|
+
// Clear all achievement progress related queries using predicate
|
|
12
|
+
// Query keys from different packages:
|
|
13
|
+
// 1. @phygitallabs/achievement (logged in): ['users', 'userAchievements', 'achievementProgressMany', ...]
|
|
14
|
+
// 2. @phygitallabs/api-core (device UID): ['userAchievements', 'achievementProgress', deviceId, ...]
|
|
15
|
+
// 3. @phygitallabs/achievement (device UID alt): ['deviceUidAchievementProgressMany', ...]
|
|
20
16
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
17
|
+
queryClient.invalidateQueries({
|
|
18
|
+
predicate: (query) => {
|
|
19
|
+
const queryKey = query.queryKey as string[];
|
|
20
|
+
return (
|
|
21
|
+
queryKey.includes("achievementProgressMany") || // For logged in users
|
|
22
|
+
queryKey.includes("achievementProgress") || // For device UID (api-core)
|
|
23
|
+
queryKey.includes("deviceUidAchievementProgressMany") // For device UID (achievement pkg)
|
|
24
|
+
);
|
|
25
|
+
},
|
|
26
|
+
refetchType: refetchActive ? (refetchInactive ? "all" : "active") : "none",
|
|
31
27
|
});
|
|
32
28
|
|
|
33
29
|
console.log("🔄 Achievement progress cache cleared");
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useAchievementTracking } from "../providers";
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { createContext, useContext, useEffect, useMemo, useCallback, useRef } from "react";
|
|
2
|
+
import { useDataTracking } from "../../data-tracking";
|
|
3
|
+
import { useNotificationStore } from "../../notification";
|
|
4
|
+
import { getActionsFromAchievementRule, AchievementType } from "../../achievement";
|
|
5
|
+
import type {
|
|
6
|
+
AchievementTrackingContextValue,
|
|
7
|
+
AchievementTrackingProviderProps,
|
|
8
|
+
AchievementActionType,
|
|
9
|
+
} from "../types";
|
|
10
|
+
import type { Achievement } from "../../achievement/types";
|
|
11
|
+
|
|
12
|
+
const AchievementTrackingContext = createContext<AchievementTrackingContextValue | undefined>(
|
|
13
|
+
undefined
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
export function AchievementTrackingProvider({
|
|
17
|
+
children,
|
|
18
|
+
achievements,
|
|
19
|
+
}: AchievementTrackingProviderProps) {
|
|
20
|
+
const { trackEvent } = useDataTracking();
|
|
21
|
+
const trackedNotificationIds = useRef(new Set<string>());
|
|
22
|
+
|
|
23
|
+
const { challenges, quests } = useMemo(() => {
|
|
24
|
+
const challengesList: Achievement[] = [];
|
|
25
|
+
const questsList: Achievement[] = [];
|
|
26
|
+
|
|
27
|
+
achievements.forEach((achievement) => {
|
|
28
|
+
if (achievement.type === AchievementType.GROUP_MISSION) {
|
|
29
|
+
questsList.push(achievement);
|
|
30
|
+
} else {
|
|
31
|
+
challengesList.push(achievement);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
challenges: challengesList,
|
|
37
|
+
quests: questsList,
|
|
38
|
+
};
|
|
39
|
+
}, [achievements]);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const unsubscribe = useNotificationStore.subscribe((state) => {
|
|
43
|
+
const notifications = state.notifications;
|
|
44
|
+
if (notifications.length === 0) return;
|
|
45
|
+
|
|
46
|
+
const latestNotification = notifications[notifications.length - 1];
|
|
47
|
+
|
|
48
|
+
if (!latestNotification?.data?.achievement) return;
|
|
49
|
+
|
|
50
|
+
const notificationId = `${latestNotification.data.achievement.id}_${Date.now()}`;
|
|
51
|
+
|
|
52
|
+
if (trackedNotificationIds.current.has(notificationId)) return;
|
|
53
|
+
|
|
54
|
+
trackedNotificationIds.current.add(notificationId);
|
|
55
|
+
|
|
56
|
+
const achievement = latestNotification.data.achievement;
|
|
57
|
+
|
|
58
|
+
if (achievement.type === AchievementType.GROUP_MISSION) {
|
|
59
|
+
trackEvent("quest_complete", {
|
|
60
|
+
quest_id: achievement.id,
|
|
61
|
+
quest_name: achievement.name,
|
|
62
|
+
});
|
|
63
|
+
} else {
|
|
64
|
+
const actions = getActionsFromAchievementRule(achievement);
|
|
65
|
+
const challengeType = actions[0] || "unknown";
|
|
66
|
+
|
|
67
|
+
trackEvent("challenge_complete", {
|
|
68
|
+
challenge_id: achievement.id,
|
|
69
|
+
challenge_name: achievement.name,
|
|
70
|
+
challenge_type: challengeType,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return () => {
|
|
76
|
+
unsubscribe();
|
|
77
|
+
};
|
|
78
|
+
}, [trackEvent]);
|
|
79
|
+
|
|
80
|
+
const trackChallengeStart = useCallback(
|
|
81
|
+
(actionType: AchievementActionType) => {
|
|
82
|
+
const matchingAchievements = challenges.filter((achievement) => {
|
|
83
|
+
const actions = getActionsFromAchievementRule(achievement);
|
|
84
|
+
return actions.includes(actionType);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (matchingAchievements.length === 0) {
|
|
88
|
+
console.error(`[AchievementTracking] No achievement found for action: ${actionType}`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
matchingAchievements.forEach((achievement) => {
|
|
93
|
+
trackEvent("challenge_start", {
|
|
94
|
+
challenge_id: achievement.id,
|
|
95
|
+
challenge_name: achievement.name,
|
|
96
|
+
challenge_type: actionType,
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
[challenges, trackEvent]
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const trackQuestStart = useCallback(() => {
|
|
104
|
+
if (quests.length === 0) {
|
|
105
|
+
console.error(`[AchievementTracking] No quests found`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
quests.forEach((quest) => {
|
|
110
|
+
trackEvent("quest_start", {
|
|
111
|
+
quest_id: quest.id,
|
|
112
|
+
quest_name: quest.name,
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}, [quests, trackEvent]);
|
|
116
|
+
|
|
117
|
+
const isReady = achievements.length > 0;
|
|
118
|
+
|
|
119
|
+
const contextValue: AchievementTrackingContextValue = {
|
|
120
|
+
trackChallengeStart,
|
|
121
|
+
trackQuestStart,
|
|
122
|
+
isReady,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<AchievementTrackingContext.Provider value={contextValue}>
|
|
127
|
+
{children}
|
|
128
|
+
</AchievementTrackingContext.Provider>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function useAchievementTracking(): AchievementTrackingContextValue {
|
|
133
|
+
const context = useContext(AchievementTrackingContext);
|
|
134
|
+
if (!context) {
|
|
135
|
+
throw new Error("useAchievementTracking must be used within AchievementTrackingProvider");
|
|
136
|
+
}
|
|
137
|
+
return context;
|
|
138
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { AchievementTrackingProvider, useAchievementTracking } from "./AchievementTrackingProvider";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
import type { Achievement } from "../../achievement/types";
|
|
3
|
+
|
|
4
|
+
export type TrackingEventName =
|
|
5
|
+
| "challenge_start"
|
|
6
|
+
| "quest_start"
|
|
7
|
+
| "challenge_complete"
|
|
8
|
+
| "quest_complete";
|
|
9
|
+
|
|
10
|
+
export type AchievementActionType = "tap_chip" | "create_memory" | "take_survey";
|
|
11
|
+
|
|
12
|
+
export interface AchievementTrackingContextValue {
|
|
13
|
+
trackChallengeStart: (actionType: AchievementActionType) => void;
|
|
14
|
+
trackQuestStart: () => void;
|
|
15
|
+
isReady: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface AchievementTrackingProviderProps {
|
|
19
|
+
children: ReactNode;
|
|
20
|
+
achievements: Achievement[];
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./hooks";
|
package/tsup.config.ts
CHANGED
|
@@ -8,6 +8,7 @@ export default defineConfig((options: Options) => ({
|
|
|
8
8
|
},
|
|
9
9
|
format: ["esm", "cjs"],
|
|
10
10
|
outExtension: ({ format }) => ({ js: format === "cjs" ? ".cjs" : ".js" }),
|
|
11
|
+
target: "es2019", // Transpile optional chaining (?.) and nullish coalescing (??) for CJS Node.js SSR compatibility
|
|
11
12
|
dts: true,
|
|
12
13
|
clean: true,
|
|
13
14
|
external: [
|