@phygitallabs/tapquest-core 6.6.0 → 6.7.0
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 +57 -32
- package/src/modules/achievement/helpers/useClearAchievementProgressCache.tsx +19 -16
- 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 +153 -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phygitallabs/tapquest-core",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.7.0",
|
|
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,45 +1,69 @@
|
|
|
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
|
-
|
|
66
|
+
return achievement.isCompleted || achievement.overallPercentage === 100;
|
|
43
67
|
}
|
|
44
68
|
|
|
45
69
|
type SnakeToCamelCase<S extends string> = S extends `${infer T}_${infer U}`
|
|
@@ -92,9 +116,10 @@ export function convertSnakeToCamel<T>(obj: T): ConvertSnakeToCamel<T> {
|
|
|
92
116
|
|
|
93
117
|
|
|
94
118
|
export {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
119
|
+
getLocationIdsFromAchievementRule,
|
|
120
|
+
getActionsFromAchievementRule,
|
|
121
|
+
getSurveyIdsFromAchievementRule,
|
|
122
|
+
isAchievementCompleted
|
|
98
123
|
}
|
|
99
124
|
|
|
100
125
|
export { useClearAchievementProgressCache } from './useClearAchievementProgressCache';
|
|
@@ -11,23 +11,26 @@ export function useClearAchievementProgressCache() {
|
|
|
11
11
|
}) => {
|
|
12
12
|
const { refetchActive = true, refetchInactive = false } = options || {};
|
|
13
13
|
|
|
14
|
-
// Clear all achievement progress related queries
|
|
15
|
-
// Query keys from packages
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
];
|
|
14
|
+
// Clear all achievement progress related queries using predicate
|
|
15
|
+
// Query keys from different packages:
|
|
16
|
+
// 1. @phygitallabs/achievement (logged in): ['users', 'userAchievements', 'achievementProgressMany', ...]
|
|
17
|
+
// 2. @phygitallabs/api-core (device UID): ['userAchievements', 'achievementProgress', deviceId, ...]
|
|
18
|
+
// 3. @phygitallabs/achievement (device UID alt): ['deviceUidAchievementProgressMany', ...]
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
20
|
+
queryClient.invalidateQueries({
|
|
21
|
+
predicate: (query) => {
|
|
22
|
+
const queryKey = query.queryKey as string[];
|
|
23
|
+
return (
|
|
24
|
+
queryKey.includes("achievementProgressMany") || // For logged in users
|
|
25
|
+
queryKey.includes("achievementProgress") || // For device UID (api-core)
|
|
26
|
+
queryKey.includes("deviceUidAchievementProgressMany") // For device UID (achievement pkg)
|
|
27
|
+
);
|
|
28
|
+
},
|
|
29
|
+
refetchType: refetchActive
|
|
30
|
+
? refetchInactive
|
|
31
|
+
? "all"
|
|
32
|
+
: "active"
|
|
33
|
+
: "none",
|
|
31
34
|
});
|
|
32
35
|
|
|
33
36
|
console.log("🔄 Achievement progress cache cleared");
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useAchievementTracking } from "../providers";
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useCallback,
|
|
7
|
+
useRef,
|
|
8
|
+
} from "react";
|
|
9
|
+
import { useDataTracking } from "../../data-tracking";
|
|
10
|
+
import { useNotificationStore } from "../../notification";
|
|
11
|
+
import {
|
|
12
|
+
getActionsFromAchievementRule,
|
|
13
|
+
AchievementType,
|
|
14
|
+
} from "../../achievement";
|
|
15
|
+
import type {
|
|
16
|
+
AchievementTrackingContextValue,
|
|
17
|
+
AchievementTrackingProviderProps,
|
|
18
|
+
AchievementActionType,
|
|
19
|
+
} from "../types";
|
|
20
|
+
import type { Achievement } from "../../achievement/types";
|
|
21
|
+
|
|
22
|
+
const AchievementTrackingContext = createContext<
|
|
23
|
+
AchievementTrackingContextValue | undefined
|
|
24
|
+
>(undefined);
|
|
25
|
+
|
|
26
|
+
export function AchievementTrackingProvider({
|
|
27
|
+
children,
|
|
28
|
+
achievements,
|
|
29
|
+
}: AchievementTrackingProviderProps) {
|
|
30
|
+
const { trackEvent } = useDataTracking();
|
|
31
|
+
const trackedNotificationIds = useRef(new Set<string>());
|
|
32
|
+
|
|
33
|
+
const { challenges, quests } = useMemo(() => {
|
|
34
|
+
const challengesList: Achievement[] = [];
|
|
35
|
+
const questsList: Achievement[] = [];
|
|
36
|
+
|
|
37
|
+
achievements.forEach((achievement) => {
|
|
38
|
+
if (achievement.type === AchievementType.GROUP_MISSION) {
|
|
39
|
+
questsList.push(achievement);
|
|
40
|
+
} else {
|
|
41
|
+
challengesList.push(achievement);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
challenges: challengesList,
|
|
47
|
+
quests: questsList,
|
|
48
|
+
};
|
|
49
|
+
}, [achievements]);
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const unsubscribe = useNotificationStore.subscribe((state) => {
|
|
53
|
+
|
|
54
|
+
const notifications = state.notifications;
|
|
55
|
+
if (notifications.length === 0) return;
|
|
56
|
+
|
|
57
|
+
const latestNotification = notifications[notifications.length - 1];
|
|
58
|
+
|
|
59
|
+
if (!latestNotification?.data?.achievement) return;
|
|
60
|
+
|
|
61
|
+
const notificationId = `${latestNotification.data.achievement.id}_${Date.now()}`;
|
|
62
|
+
|
|
63
|
+
if (trackedNotificationIds.current.has(notificationId)) return;
|
|
64
|
+
|
|
65
|
+
trackedNotificationIds.current.add(notificationId);
|
|
66
|
+
|
|
67
|
+
const achievement = latestNotification.data.achievement;
|
|
68
|
+
|
|
69
|
+
if (achievement.type === AchievementType.GROUP_MISSION) {
|
|
70
|
+
trackEvent("quest_complete", {
|
|
71
|
+
quest_id: achievement.id,
|
|
72
|
+
quest_name: achievement.name,
|
|
73
|
+
});
|
|
74
|
+
} else {
|
|
75
|
+
const actions = getActionsFromAchievementRule(achievement);
|
|
76
|
+
const challengeType = actions[0] || "unknown";
|
|
77
|
+
|
|
78
|
+
trackEvent("challenge_complete", {
|
|
79
|
+
challenge_id: achievement.id,
|
|
80
|
+
challenge_name: achievement.name,
|
|
81
|
+
challenge_type: challengeType,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return () => {
|
|
87
|
+
unsubscribe();
|
|
88
|
+
};
|
|
89
|
+
}, [trackEvent]);
|
|
90
|
+
|
|
91
|
+
const trackChallengeStart = useCallback(
|
|
92
|
+
(actionType: AchievementActionType) => {
|
|
93
|
+
const matchingAchievements = challenges.filter((achievement) => {
|
|
94
|
+
const actions = getActionsFromAchievementRule(achievement);
|
|
95
|
+
return actions.includes(actionType);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (matchingAchievements.length === 0) {
|
|
99
|
+
console.error(
|
|
100
|
+
`[AchievementTracking] No achievement found for action: ${actionType}`
|
|
101
|
+
);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
matchingAchievements.forEach((achievement) => {
|
|
106
|
+
trackEvent("challenge_start", {
|
|
107
|
+
challenge_id: achievement.id,
|
|
108
|
+
challenge_name: achievement.name,
|
|
109
|
+
challenge_type: actionType,
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
[challenges, trackEvent]
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const trackQuestStart = useCallback(() => {
|
|
117
|
+
if (quests.length === 0) {
|
|
118
|
+
console.error(`[AchievementTracking] No quests found`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
quests.forEach((quest) => {
|
|
123
|
+
trackEvent("quest_start", {
|
|
124
|
+
quest_id: quest.id,
|
|
125
|
+
quest_name: quest.name,
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}, [quests, trackEvent]);
|
|
129
|
+
|
|
130
|
+
const isReady = achievements.length > 0;
|
|
131
|
+
|
|
132
|
+
const contextValue: AchievementTrackingContextValue = {
|
|
133
|
+
trackChallengeStart,
|
|
134
|
+
trackQuestStart,
|
|
135
|
+
isReady,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<AchievementTrackingContext.Provider value={contextValue}>
|
|
140
|
+
{children}
|
|
141
|
+
</AchievementTrackingContext.Provider>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function useAchievementTracking(): AchievementTrackingContextValue {
|
|
146
|
+
const context = useContext(AchievementTrackingContext);
|
|
147
|
+
if (!context) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
"useAchievementTracking must be used within AchievementTrackingProvider"
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
return context;
|
|
153
|
+
}
|
|
@@ -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";
|