@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phygitallabs/tapquest-core",
3
- "version": "6.6.0",
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": "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,46 +1,70 @@
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;
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
- [K in keyof T as K extends string
54
- ? SnakeToCamelCase<K>
55
- : K]: ConvertSnakeToCamel<T[K]>;
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
- if (obj === null || obj === undefined) {
64
- return obj as ConvertSnakeToCamel<T>;
65
- }
66
-
67
- if (Array.isArray(obj)) {
68
- return obj.map((item) =>
69
- convertSnakeToCamel(item)
70
- ) as ConvertSnakeToCamel<T>;
71
- }
72
-
73
- if (typeof obj === "object" && obj.constructor === Object) {
74
- const converted: Record<string, unknown> = {};
75
-
76
- for (const key in obj) {
77
- if (obj.hasOwnProperty(key)) {
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 obj as ConvertSnakeToCamel<T>;
102
+
103
+ return converted as ConvertSnakeToCamel<T>;
91
104
  }
92
-
93
105
 
94
- export {
95
- getLocationIdsFromAchievementRule,
96
- getActionsFromAchievementRule,
97
- isAchievementCompleted
106
+ return obj as ConvertSnakeToCamel<T>;
98
107
  }
99
108
 
100
- export { useClearAchievementProgressCache } from './useClearAchievementProgressCache';
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/achievement/src/hooks/useAchievement.ts
16
- const queryKeyPatterns = [
17
- ["deviceUidAchievementProgressMany"], // Device UID progress (line 69)
18
- ["achievementProgressMany"], // All achievements (line 39)
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
- 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
- });
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,3 @@
1
+ export * from "./providers";
2
+ export * from "./hooks";
3
+ export * from "./types";
@@ -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,3 @@
1
+ import { useCreateClickButtonActionLog } from "@phygitallabs/api-core";
2
+
3
+ export { useCreateClickButtonActionLog };
@@ -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: [