@merkaly/nuxt 0.9.1 → 0.9.3

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/README.md CHANGED
@@ -22,6 +22,19 @@ A Nuxt module that provides Auth0 authentication integration and API utilities f
22
22
 
23
23
  Install the module to your Nuxt application:
24
24
 
25
+ ## Authentication and tenant boot
26
+
27
+ Every surface can sign in; `auth0.requiresAuth` only decides whether pages require a login (globally, or per page with `definePageMeta({ requiresAuth: true })`). `auth0.requiresTenant` decides whether API calls require a tenant (`global: true` adapters bypass it).
28
+
29
+ On boot the module restores an existing session (user and token) and then runs the app hooks in order, `merkaly:tenant` then `merkaly:auth`, with that user (or none). `MKApp` shows its `loading` slot until both finish; if a hook throws, it shows the `error` slot instead. The auth callback page renders right away so it can complete the login.
30
+
31
+ | Surface | Login | Tenant | Boot |
32
+ |---|---|---|---|
33
+ | admin | required | required | `merkaly:tenant` creates the session and sets the tenant; without a session the auth middleware redirects to login. |
34
+ | dashboard | required | not used | `merkaly:auth` loads the session; without a session it redirects to login. |
35
+ | store | optional | required | `merkaly:tenant` resolves the store from the host (public); a signed-in buyer is restored too; protected pages (account, checkout) ask for login. |
36
+ | www | optional | not used | No hooks; anonymous or signed-in. |
37
+
25
38
  ## Money formatting
26
39
 
27
40
  Money in Merkaly is stored as **integer cents**. `MKFormatMoney`, `MKInputMoney`,
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.14.0"
6
6
  },
7
- "version": "0.9.1",
7
+ "version": "0.9.3",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
@@ -81,8 +81,8 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
81
81
  }>, {
82
82
  mode: PlaceTypes;
83
83
  disabled: boolean;
84
- placeholder: string;
85
84
  countries: string[];
85
+ placeholder: string;
86
86
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
87
87
  declare const _default: typeof __VLS_export;
88
88
  export default _default;
@@ -81,8 +81,8 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
81
81
  }>, {
82
82
  mode: PlaceTypes;
83
83
  disabled: boolean;
84
- placeholder: string;
85
84
  countries: string[];
85
+ placeholder: string;
86
86
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
87
87
  declare const _default: typeof __VLS_export;
88
88
  export default _default;
@@ -1,18 +1,20 @@
1
1
  <script setup>
2
2
  import { computed, onBeforeMount, ref } from "vue";
3
+ import { useI18n } from "vue-i18n";
3
4
  import FormatIcon from "../format/FormatIcon.vue";
4
5
  const props = defineProps({
5
6
  disabled: { type: Boolean, default: false },
6
7
  preset: { type: String, default: () => "today" }
7
8
  });
8
9
  const model = defineModel({ type: Object });
10
+ const { locale, t } = useI18n({ useScope: "local" });
9
11
  const presets = [
10
- { key: "today", label: "Today", icon: "calendar-day" },
11
- { key: "yesterday", label: "Yesterday", icon: "calendar-minus" },
12
- { key: "last7days", label: "Last 7 days", icon: "calendar-week" },
13
- { key: "last30days", label: "Last 30 days", icon: "calendar" },
14
- { key: "thisMonth", label: "This month", icon: "calendar-check" },
15
- { key: "lastMonth", label: "Last month", icon: "calendar-xmark" }
12
+ { key: "today", icon: "calendar-day" },
13
+ { key: "yesterday", icon: "calendar-minus" },
14
+ { key: "last7days", icon: "calendar-week" },
15
+ { key: "last30days", icon: "calendar" },
16
+ { key: "thisMonth", icon: "calendar-check" },
17
+ { key: "lastMonth", icon: "calendar-xmark" }
16
18
  ];
17
19
  const selectedPreset = ref(model.value?.preset || props.preset);
18
20
  const getDateRange = (key) => {
@@ -50,16 +52,17 @@ const getDateRange = (key) => {
50
52
  }
51
53
  };
52
54
  const currentPresetLabel = computed(() => {
53
- return presets.find((p) => p.key === selectedPreset.value)?.label || "Select";
55
+ const preset = presets.find((p) => p.key === selectedPreset.value);
56
+ return preset ? t(preset.key) : t("select");
54
57
  });
55
58
  const formatDateRange = computed(() => {
56
59
  const range = getDateRange(selectedPreset.value);
57
60
  const options = { day: "numeric", month: "short" };
58
61
  if (range.start.getTime() === range.end.getTime()) {
59
- return range.start.toLocaleDateString("en", { ...options, year: "numeric" });
62
+ return range.start.toLocaleDateString(locale.value, { ...options, year: "numeric" });
60
63
  }
61
- const startStr = range.start.toLocaleDateString("en", options);
62
- const endStr = range.end.toLocaleDateString("en", { ...options, year: "numeric" });
64
+ const startStr = range.start.toLocaleDateString(locale.value, options);
65
+ const endStr = range.end.toLocaleDateString(locale.value, { ...options, year: "numeric" });
63
66
  return `${startStr} - ${endStr}`;
64
67
  });
65
68
  const selectPreset = (preset) => {
@@ -78,14 +81,14 @@ onBeforeMount(() => selectPreset(props.preset));
78
81
  </template>
79
82
 
80
83
  <div class="px-3 py-2 border-bottom">
81
- <span class="fs-7 fw-bold text-gray-800">Date Range</span>
84
+ <span class="fs-7 fw-bold text-gray-800" v-text="t('title')" />
82
85
  </div>
83
86
 
84
87
  <template v-for="item in presets" :key="item.key">
85
88
  <BDropdownItem :active="selectedPreset === item.key" @click="selectPreset(item.key)">
86
89
  <div class="d-flex align-items-center gap-2">
87
90
  <FormatIcon :name="item.icon" class="text-gray-500" />
88
- <span v-text="item.label" />
91
+ <span v-text="t(item.key)" />
89
92
  </div>
90
93
  </BDropdownItem>
91
94
  </template>
@@ -95,8 +98,35 @@ onBeforeMount(() => selectPreset(props.preset));
95
98
  <BDropdownItem disabled>
96
99
  <div class="d-flex align-items-center gap-2 text-muted">
97
100
  <FormatIcon name="calendar-plus" />
98
- <span>Custom range (coming soon)</span>
101
+ <span v-text="t('custom')" />
99
102
  </div>
100
103
  </BDropdownItem>
101
104
  </BDropdown>
102
105
  </template>
106
+
107
+ <i18n lang="json">
108
+ {
109
+ "en": {
110
+ "title": "Date range",
111
+ "select": "Select",
112
+ "today": "Today",
113
+ "yesterday": "Yesterday",
114
+ "last7days": "Last 7 days",
115
+ "last30days": "Last 30 days",
116
+ "thisMonth": "This month",
117
+ "lastMonth": "Last month",
118
+ "custom": "Custom range (coming soon)"
119
+ },
120
+ "pt": {
121
+ "title": "Período",
122
+ "select": "Selecionar",
123
+ "today": "Hoje",
124
+ "yesterday": "Ontem",
125
+ "last7days": "Últimos 7 dias",
126
+ "last30days": "Últimos 30 dias",
127
+ "thisMonth": "Este mês",
128
+ "lastMonth": "Mês passado",
129
+ "custom": "Período personalizado (em breve)"
130
+ }
131
+ }
132
+ </i18n>
@@ -10,8 +10,13 @@ export const useAuth = () => {
10
10
  const clear = () => error.value = void 0;
11
11
  const logout = () => nuxtApp.$auth0.logout();
12
12
  const retry = async () => {
13
+ isLoading.value = true;
13
14
  clear();
14
- await nuxtApp.callHook("merkaly:auth:retry");
15
+ try {
16
+ await nuxtApp.callHook("merkaly:auth:retry");
17
+ } finally {
18
+ isLoading.value = false;
19
+ }
15
20
  };
16
21
  const fail = async (reason) => {
17
22
  error.value = reason;
@@ -1,10 +1,11 @@
1
- import { defineNuxtRouteMiddleware, navigateTo, useRuntimeConfig } from "#app";
1
+ import { defineNuxtRouteMiddleware, navigateTo, useRuntimeConfig, useState } from "#app";
2
2
  import { useAuth } from "../composables/useAuth.js";
3
3
  export default defineNuxtRouteMiddleware((to) => {
4
4
  const { isLoading, isAuthenticated } = useAuth();
5
+ const isStarting = useState("auth:starting", () => false);
5
6
  const { public: { merkaly } } = useRuntimeConfig();
6
7
  const callbackPath = URL.canParse(merkaly.auth0.callbackUrl) ? new URL(merkaly.auth0.callbackUrl).pathname : merkaly.auth0.callbackUrl;
7
- if (isLoading.value) {
8
+ if (isLoading.value && !isStarting.value) {
8
9
  return;
9
10
  }
10
11
  if (to.path === callbackPath) {
@@ -1,5 +1,5 @@
1
1
  import { createAuth0Client } from "@auth0/auth0-spa-js";
2
- import { defineNuxtPlugin, useRuntimeConfig, useNuxtApp } from "#imports";
2
+ import { defineNuxtPlugin, useRuntimeConfig, useNuxtApp, useState } from "#imports";
3
3
  import { navigateTo } from "#app";
4
4
  import { defu } from "defu";
5
5
  import { useAuth } from "../composables/useAuth.js";
@@ -13,7 +13,8 @@ export default defineNuxtPlugin(async ({ callHook, hook }) => {
13
13
  domain: $config.merkaly.auth0.domain,
14
14
  useRefreshTokens: true
15
15
  });
16
- const { isLoading, user, token, fail } = useAuth();
16
+ const { error, isLoading, user, token, fail } = useAuth();
17
+ const isStarting = useState("auth:starting", () => false);
17
18
  const self0 = Object.assign(Object.create(Object.getPrototypeOf(auth0)), auth0);
18
19
  let tokenError;
19
20
  auth0.getUser = () => self0.getUser().then((result) => user.value = result).catch((reason) => console.error("[Auth0] getUser failed", reason));
@@ -26,8 +27,14 @@ export default defineNuxtPlugin(async ({ callHook, hook }) => {
26
27
  });
27
28
  const resolveIdentity = () => Promise.allSettled([auth0.getUser(), auth0.getTokenSilently()]);
28
29
  const initTenantContext = async () => {
29
- await callHook("merkaly:tenant", user.value);
30
- await callHook("merkaly:auth", user.value);
30
+ try {
31
+ await callHook("merkaly:tenant", user.value);
32
+ await callHook("merkaly:auth", user.value);
33
+ } catch (reason) {
34
+ if (!error.value) {
35
+ await fail({ message: reason?.message || String(reason), stage: "tenant" });
36
+ }
37
+ }
31
38
  };
32
39
  const startSession = async () => {
33
40
  if (user.value && !token.value && tokenError && !NO_SESSION_ERRORS.includes(tokenError.error)) {
@@ -81,10 +88,19 @@ export default defineNuxtPlugin(async ({ callHook, hook }) => {
81
88
  return await $api("/identities", { body, global: true, method: "POST" });
82
89
  };
83
90
  const isAuthCallback = window.location.pathname === callbackPath;
84
- if (!isAuthCallback) {
85
- await resolveIdentity();
86
- hook("app:created", startSession);
91
+ if (isAuthCallback) {
92
+ isLoading.value = false;
93
+ return { provide: { auth0 } };
87
94
  }
88
- isLoading.value = false;
95
+ await resolveIdentity();
96
+ isStarting.value = true;
97
+ hook("app:created", async () => {
98
+ try {
99
+ await startSession();
100
+ } finally {
101
+ isStarting.value = false;
102
+ isLoading.value = false;
103
+ }
104
+ });
89
105
  return { provide: { auth0 } };
90
106
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@merkaly/nuxt",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/merkaly-io/nuxt.git"
@@ -54,7 +54,7 @@
54
54
  "filesize": "^11.0.17",
55
55
  "gsap": "^3.15.0",
56
56
  "notivue": "^2.4.5",
57
- "nuxt": "^4.6.2",
57
+ "nuxt": "^4.6.3",
58
58
  "reflect-metadata": "^0.2.2",
59
59
  "sass": "^1.106.1",
60
60
  "typescript": "~5.9.3",