@phygitallabs/tapquest-core 6.5.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.5.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
+ });
37
46
  });
38
- return Array.from(new Set(actions)) as string[];
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
+ }
60
+ });
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,7 +116,10 @@ export function convertSnakeToCamel<T>(obj: T): ConvertSnakeToCamel<T> {
92
116
 
93
117
 
94
118
  export {
95
- getLocationIdsFromAchievementRule,
96
- getActionsFromAchievementRule,
97
- isAchievementCompleted
98
- }
119
+ getLocationIdsFromAchievementRule,
120
+ getActionsFromAchievementRule,
121
+ getSurveyIdsFromAchievementRule,
122
+ isAchievementCompleted
123
+ }
124
+
125
+ export { useClearAchievementProgressCache } from './useClearAchievementProgressCache';
@@ -0,0 +1,43 @@
1
+ import { useQueryClient } from "@tanstack/react-query";
2
+ import { useCallback } from "react";
3
+
4
+ export function useClearAchievementProgressCache() {
5
+ const queryClient = useQueryClient();
6
+
7
+ const clearCache = useCallback(
8
+ (options?: {
9
+ refetchActive?: boolean;
10
+ refetchInactive?: boolean;
11
+ }) => {
12
+ const { refetchActive = true, refetchInactive = false } = options || {};
13
+
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', ...]
19
+
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",
34
+ });
35
+
36
+ console.log("🔄 Achievement progress cache cleared");
37
+ },
38
+ [queryClient]
39
+ );
40
+ return {
41
+ clearCache,
42
+ };
43
+ }
@@ -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";