@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phygitallabs/tapquest-core",
3
- "version": "6.6.0",
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": "latest",
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 type { APIVersionType, EnvironmentType } from "./types/common";
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
- 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
- }
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
- return Array.from(new Set(locationIds)) as string[];
23
+ });
24
+ return Array.from(new Set(locationIds)) as string[];
25
25
  };
26
26
 
27
- const getActionsFromAchievementRule = (achievement: Achievement) => {
28
- if (!achievement.rule) return [];
29
- const actions: string[] = [];
30
- Object.values(achievement.rule).forEach((ruleList) => {
31
- if (!ruleList.rules) return;
32
- ruleList.rules.forEach((rule) => {
33
- if (rule.action) {
34
- actions.push(rule.action)
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
- return Array.from(new Set(actions)) as string[];
61
+ });
62
+ return Array.from(new Set(actions)) as string[];
39
63
  };
40
64
 
41
65
  const isAchievementCompleted = (achievement: UserAchievementProgress) => {
42
- return achievement.isCompleted || achievement.overallPercentage === 100;
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
- getLocationIdsFromAchievementRule,
96
- getActionsFromAchievementRule,
97
- isAchievementCompleted
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/achievement/src/hooks/useAchievement.ts
16
- const queryKeyPatterns = [
17
- ["deviceUidAchievementProgressMany"], // Device UID progress (line 69)
18
- ["achievementProgressMany"], // All achievements (line 39)
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
- queryKeyPatterns.forEach((queryKey) => {
22
- // Invalidate the queries to mark them as stale
23
- queryClient.invalidateQueries({
24
- queryKey,
25
- refetchType: refetchActive
26
- ? refetchInactive
27
- ? "all"
28
- : "active"
29
- : "none",
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,3 @@
1
+ export * from "./providers";
2
+ export * from "./hooks";
3
+ export * from "./types";
@@ -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,3 @@
1
+ import { useCreateClickButtonActionLog } from "@phygitallabs/api-core";
2
+
3
+ export { useCreateClickButtonActionLog };
@@ -0,0 +1 @@
1
+ export * from "./hooks";