@mundogamernetwork/shared-ui 1.13.3 → 1.13.5

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.
@@ -5,13 +5,15 @@ import { fetchGamificationSummary, type GamificationSummary } from "../../servic
5
5
  const props = withDefaults(
6
6
  defineProps<{
7
7
  /**
8
- * Where to send the user to see and claim their tasks. Only Community
9
- * hosts those pages, so leaving this unset points at Community — which
10
- * is the right destination from every other front. Community itself
11
- * passes its own internal route to avoid a full page load.
8
+ * Where to send the user to see and claim their tasks. The missions
9
+ * list is centralized on network-accounts (one page, every platform,
10
+ * with cross-vertical filters) leaving this unset points there,
11
+ * which is the right destination from every front. A front that still
12
+ * has its own local missions page should pass its own internal route
13
+ * here to avoid a full page load; most no longer do.
12
14
  */
13
15
  missionsUrl?: string;
14
- /** Locale segment for the default Community destination. */
16
+ /** Locale segment for the default network-accounts destination. */
15
17
  locale?: string;
16
18
  /** Renders without the title line, for tight spaces. */
17
19
  compact?: boolean;
@@ -23,17 +25,56 @@ const props = withDefaults(
23
25
  mgcBalance?: number | null;
24
26
  /** Hide the balance where the host already shows it elsewhere. */
25
27
  showBalance?: boolean;
28
+ /**
29
+ * Pre-fetched summary from a call the host already makes elsewhere
30
+ * (e.g. its own dashboard composable, through its own configured HTTP
31
+ * client). When set — even to `null` while the host's own fetch is
32
+ * still resolving — this component skips its internal fetch entirely,
33
+ * avoiding a second request to the same endpoint through a second,
34
+ * independently-configured base URL that can drift from the host's
35
+ * own over time. Leave unset (`undefined`) to keep the original
36
+ * self-fetching behaviour.
37
+ */
38
+ summary?: GamificationSummary | null;
26
39
  }>(),
27
- { missionsUrl: "", locale: "", compact: false, mgcBalance: null, showBalance: true },
40
+ { missionsUrl: "", locale: "", compact: false, mgcBalance: null, showBalance: true, summary: undefined },
28
41
  );
29
42
 
30
43
  const emit = defineEmits<{ (e: "navigate"): void }>();
31
44
 
32
- /** Missions and the reward store live on Community for the whole ecosystem. */
33
- const COMMUNITY_URL = "https://mundogamer.community";
45
+ /** The centralized missions page lives on network-accounts for the whole ecosystem. */
46
+ const ACCOUNTS_URL = "https://accounts.mundogamer.network";
47
+
48
+ /**
49
+ * Dig out the payload regardless of how many envelopes wrap it.
50
+ *
51
+ * The API nests deeper than its own sendResponse() suggests — the gateway adds
52
+ * a second {message,data,status_code} layer on top, so the payload sits at
53
+ * response.data.data.data rather than response.data.data. Reading a fixed
54
+ * depth silently returned the inner envelope, which has no `level`, so the bar
55
+ * loaded and vanished on a perfectly good 200. Walking down until the shape
56
+ * appears survives either nesting.
57
+ */
58
+ function unwrapSummary(payload: any): GamificationSummary | null {
59
+ let node = payload;
60
+
61
+ for (let depth = 0; depth < 4 && node && typeof node === "object"; depth += 1) {
62
+ if (node.level || node.wallet || node.missions) return node as GamificationSummary;
63
+ node = node.data;
64
+ }
65
+
66
+ return null;
67
+ }
68
+
69
+ const fetchedSummary = ref<GamificationSummary | null>(null);
70
+ const selfLoaded = ref(false);
34
71
 
35
- const summary = ref<GamificationSummary | null>(null);
36
- const loaded = ref(false);
72
+ /** The host manages this prop as soon as it's bound, even before its own fetch resolves — `undefined` only when the prop was never passed at all. */
73
+ const isHostManaged = computed(() => props.summary !== undefined);
74
+ const summary = computed(() => (isHostManaged.value ? props.summary ?? null : fetchedSummary.value));
75
+ // Host-managed: loaded once the host's data actually arrives (an unresolved
76
+ // host fetch must keep showing the skeleton, not the empty/hidden state).
77
+ const loaded = computed(() => (isHostManaged.value ? summary.value !== null : selfLoaded.value));
37
78
 
38
79
  const level = computed(() => summary.value?.level ?? null);
39
80
  const claimable = computed(() => summary.value?.missions?.claimable_count ?? 0);
@@ -49,13 +90,18 @@ const destination = computed(() => {
49
90
  (typeof document !== "undefined" ? document.documentElement.lang : "") ||
50
91
  "en";
51
92
 
52
- return `${COMMUNITY_URL}/${locale}/gamification/missions`;
93
+ return `${ACCOUNTS_URL}/${locale}/account/missions`;
53
94
  });
54
95
 
55
96
  const balance = computed(() => {
56
97
  const fromHost = props.mgcBalance;
57
98
  const value = fromHost !== null && fromHost !== undefined ? fromHost : summary.value?.wallet?.balance;
58
- return typeof value === "number" ? Math.floor(value).toLocaleString() : null;
99
+ // The API sends the balance as a decimal string ("1801.50"), not a number.
100
+ const numeric = typeof value === "string" ? Number(value) : value;
101
+
102
+ return typeof numeric === "number" && Number.isFinite(numeric)
103
+ ? Math.floor(numeric).toLocaleString()
104
+ : null;
59
105
  });
60
106
 
61
107
  // progress_percent comes from the API; the local computation is a fallback for
@@ -69,18 +115,20 @@ const percent = computed(() => {
69
115
  });
70
116
 
71
117
  onMounted(async () => {
118
+ if (isHostManaged.value) return; // host supplies (and reactively updates) props.summary itself
119
+
72
120
  try {
73
121
  const response = await fetchGamificationSummary();
74
- summary.value = response?.data?.data ?? response?.data ?? null;
122
+ fetchedSummary.value = unwrapSummary(response?.data);
75
123
  } catch (error) {
76
124
  // Never let a gamification hiccup break the header it sits in — but say
77
125
  // so. Swallowing this silently made a deployed bar that renders nothing
78
126
  // indistinguishable from a bar that was never deployed, and cost hours
79
127
  // of bundle forensics to tell apart.
80
128
  console.warn("[MgXpLevelBar] gamification dashboard unavailable", error);
81
- summary.value = null;
129
+ fetchedSummary.value = null;
82
130
  } finally {
83
- loaded.value = true;
131
+ selfLoaded.value = true;
84
132
  }
85
133
  });
86
134
  </script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mundogamernetwork/shared-ui",
3
- "version": "1.13.3",
3
+ "version": "1.13.5",
4
4
  "description": "Mundo Gamer Network - Shared UI Layer (Nuxt 3)",
5
5
  "type": "module",
6
6
  "main": "./nuxt.config.ts",
@@ -18,7 +18,8 @@ export interface XpLevel {
18
18
 
19
19
  export interface GamificationSummary {
20
20
  level: XpLevel;
21
- wallet: { balance: number; lifetime_earned: number };
21
+ /** The API serialises these as decimal strings, not numbers. */
22
+ wallet: { balance: number | string; lifetime_earned: number | string };
22
23
  missions: { active: any[]; claimable_count: number };
23
24
  achievements: { total_unlocked: number; recent: any[] };
24
25
  }
@@ -37,6 +37,19 @@ export function getHttpService(): AxiosInstance {
37
37
  withCredentials: true,
38
38
  });
39
39
 
40
+ // Dev-only bearer token injection lives on `defaults.headers.common` rather
41
+ // than inside the request interceptor below: axios 1.19's `mergeConfig`
42
+ // (invoked on every dispatch via resolveConfig.js) converts an AxiosHeaders
43
+ // instance to a plain object via `{ ...thing }` before merging, and an
44
+ // Authorization value set inside a request interceptor does not survive
45
+ // that spread — it never reaches the server. Already discovered and fixed
46
+ // the same way in jobs-frontend's own httpService.ts; this file had the
47
+ // same bug independently. `defaults.headers.common` goes through axios's
48
+ // own default-header merge path instead, which does not hit it.
49
+ if (import.meta.env.VITE_APP_ENV !== "production" && import.meta.env.VITE_BEARER_TOKEN) {
50
+ _httpService.defaults.headers.common.Authorization = `Bearer ${import.meta.env.VITE_BEARER_TOKEN}`;
51
+ }
52
+
40
53
  _httpService.interceptors.response.use(
41
54
  (response) => response,
42
55
  (error) => {
@@ -88,9 +101,6 @@ export function getHttpService(): AxiosInstance {
88
101
  config.params = { ...config.params, lang };
89
102
 
90
103
  if (import.meta.env.VITE_APP_ENV !== "production" && typeof window !== "undefined") {
91
- if (import.meta.env.VITE_BEARER_TOKEN) {
92
- config.headers.Authorization = `Bearer ${import.meta.env.VITE_BEARER_TOKEN}`;
93
- }
94
104
  config.headers.set("X-Timezone", Intl.DateTimeFormat().resolvedOptions().timeZone);
95
105
  }
96
106