@ossy/app 3.11.0 → 3.12.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/README.md CHANGED
@@ -76,7 +76,7 @@ import { CloudLight } from '@ossy/themes'
76
76
  export default {
77
77
  workspaceId: 'your-workspace-id',
78
78
  theme: CloudLight,
79
- apiUrl: 'https://api.ossy.se/api/v0',
79
+ apiUrl: '/api/v0',
80
80
  }
81
81
  ```
82
82
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ossy/app",
3
- "version": "3.11.0",
3
+ "version": "3.12.0",
4
4
  "description": "",
5
5
  "source": "./src/index.js",
6
6
  "main": "./src/index.js",
@@ -49,21 +49,22 @@
49
49
  "@babel/eslint-parser": "^7.15.8",
50
50
  "@babel/preset-react": "^7.26.3",
51
51
  "@babel/register": "^7.25.9",
52
- "@ossy/authentication": "^3.11.0",
52
+ "@ossy/authentication": "^3.11.1",
53
53
  "@ossy/design-system": "^3.11.0",
54
54
  "@ossy/locale": "^3.4.0",
55
55
  "@ossy/manifest": "^3.9.0",
56
- "@ossy/package-catalog": "^3.11.0",
56
+ "@ossy/package-catalog": "^3.12.0",
57
57
  "@ossy/pages": "^3.0.9",
58
- "@ossy/platform": "^3.11.0",
59
- "@ossy/resources": "^3.11.0",
58
+ "@ossy/platform": "^3.12.0",
59
+ "@ossy/resources": "^3.12.0",
60
60
  "@ossy/router": "^3.0.9",
61
61
  "@ossy/router-react": "^3.11.0",
62
62
  "@ossy/schema": "^3.8.0",
63
- "@ossy/sdk": "^3.5.0",
64
- "@ossy/sdk-react": "^3.11.0",
63
+ "@ossy/sdk": "^3.12.0",
64
+ "@ossy/sdk-react": "^3.12.0",
65
65
  "@ossy/themes": "^3.8.0",
66
- "@ossy/workspaces": "^3.11.0",
66
+ "@ossy/users": "^3.12.0",
67
+ "@ossy/workspaces": "^3.12.0",
67
68
  "@rollup/plugin-alias": "^6.0.0",
68
69
  "@rollup/plugin-babel": "^7.1.0",
69
70
  "@rollup/plugin-commonjs": "^29.0.0",
@@ -97,5 +98,5 @@
97
98
  "README.md",
98
99
  "tsconfig.json"
99
100
  ],
100
- "gitHead": "53ff8456920e09151c9d88df4a6c3ee958d811eb"
101
+ "gitHead": "c2650803219ad1831559b512848358d9550ffad2"
101
102
  }
package/src/shell/App.jsx CHANGED
@@ -11,6 +11,7 @@ import { PageViewTracker } from './PageViewTracker.jsx'
11
11
  import { defaultAppSettings } from './AppSettings.jsx'
12
12
  import { Router } from '@ossy/router-react'
13
13
  import { AppContext } from './AppContext.js'
14
+ import { UserSetupGuard } from './UserSetupGuard.jsx'
14
15
 
15
16
  export const App = ({ children, language, messages, fallbackMessages, sdk: sdkOverride, ..._appSettings }) => {
16
17
  const appSettings = { ...defaultAppSettings(), ..._appSettings }
@@ -43,7 +44,9 @@ export const App = ({ children, language, messages, fallbackMessages, sdk: sdkOv
43
44
  <WorkspaceAppSettingsSync />
44
45
  <Router {...appSettings} pages={appSettings.pages || []}>
45
46
  <PageViewTracker />
46
- {children}
47
+ <UserSetupGuard>
48
+ {children}
49
+ </UserSetupGuard>
47
50
  {appSettings.devMode && <ThemeEditor />}
48
51
  </Router>
49
52
  </WorkspaceProvider>
@@ -1,8 +1,9 @@
1
1
  import { useEffect } from 'react'
2
2
  import { useRouter } from '@ossy/router-react'
3
3
  import { useSdk } from '@ossy/sdk-react'
4
- import { GetWorkspace } from '@ossy/workspaces'
5
- import { CreatePageView } from '@ossy/resources'
4
+ import { getOrCreateVisitorId } from './visitor-id.js'
5
+
6
+ const CreatePageView = { id: '@ossy/analytics/actions/create-page-view' }
6
7
 
7
8
  /** Survives Strict Mode remounts so the same navigation is not double-counted. */
8
9
  let lastRecordedKey = null
@@ -10,23 +11,22 @@ let lastRecordedAt = 0
10
11
  const DEDUPE_MS = 1500
11
12
 
12
13
  /**
13
- * Emit a page-view resource on client navigations so analytics can aggregate traffic.
14
- * No UImount once under the app shell / router.
14
+ * Emit a page-view on client navigations (anonymous and signed-in).
15
+ * Public ingest on `@ossy/analytics` invoked by id so the shell does not
16
+ * depend on that feature package. Failures are silent.
15
17
  */
16
18
  export function PageViewTracker () {
17
19
  const router = useRouter()
18
20
  const sdk = useSdk()
19
- const { data: workspace } = sdk.read(GetWorkspace)
20
21
 
21
22
  useEffect(() => {
22
23
  if (typeof window === 'undefined') return
23
- if (!workspace?.id) return
24
24
 
25
25
  const path = window.location.pathname || '/'
26
26
  const section = window.location.hash
27
27
  ? window.location.hash.replace('#', '')
28
28
  : undefined
29
- const key = `${workspace.id}:${path}:${section || ''}:${router.language || ''}`
29
+ const key = `${path}:${section || ''}:${router.language || ''}`
30
30
  const now = Date.now()
31
31
  if (lastRecordedKey === key && now - lastRecordedAt < DEDUPE_MS) return
32
32
  lastRecordedKey = key
@@ -37,9 +37,9 @@ export function PageViewTracker () {
37
37
  section,
38
38
  language: router.language,
39
39
  referrer: typeof document !== 'undefined' ? (document.referrer || undefined) : undefined,
40
- eventAt: now,
40
+ visitorId: getOrCreateVisitorId(),
41
41
  }).catch(() => {})
42
- }, [sdk, router.href, router.language, workspace?.id])
42
+ }, [sdk, router.href, router.language])
43
43
 
44
44
  return null
45
45
  }
@@ -0,0 +1,27 @@
1
+ import React from 'react'
2
+ import { GetCurrentUser } from '@ossy/authentication'
3
+ import { CompleteUserNamePrompt, outstandingUserRequirements } from '@ossy/users'
4
+ import { useSdk, AsyncStatus } from '@ossy/sdk-react'
5
+ import { useApp } from './AppContext.js'
6
+
7
+ /**
8
+ * After auth, block the current page until outstanding user requirements are met.
9
+ * Anonymous visits (public booking, sign-in, invite verify) are unchanged:
10
+ * `isAuthenticated` is still false on those first loads.
11
+ */
12
+ export function UserSetupGuard ({ children }) {
13
+ const app = useApp()
14
+ const sdk = useSdk()
15
+ const enabled = Boolean(app?.isAuthenticated)
16
+ const { status, data: user, refetch } = sdk.read(GetCurrentUser, undefined, { enabled })
17
+
18
+ if (!enabled) return children
19
+ if (status !== AsyncStatus.Success || !user) return children
20
+
21
+ const nextRequirement = outstandingUserRequirements(user)[0]
22
+ if (nextRequirement === 'name') {
23
+ return <CompleteUserNamePrompt onCompleted={() => refetch()} />
24
+ }
25
+
26
+ return children
27
+ }
@@ -1,3 +1,5 @@
1
+ import { resolveActionsUrl } from '@ossy/sdk'
2
+
1
3
  /**
2
4
  * Resolve integration endpoint URLs from app context and browser origin.
3
5
  *
@@ -19,14 +21,9 @@ export function resolveEndpoints (app) {
19
21
  : ''
20
22
 
21
23
  const normalizedApi = apiUrl.replace(/\/$/, '')
22
- let actionsUrl = '/actions'
23
-
24
- if (normalizedApi.startsWith('http://') || normalizedApi.startsWith('https://')) {
25
- actionsUrl = `${normalizedApi}/actions`
26
- } else if (normalizedApi.startsWith('/')) {
27
- actionsUrl = appOrigin ? `${appOrigin}${normalizedApi}/actions` : `${normalizedApi}/actions`
28
- } else if (appOrigin) {
29
- actionsUrl = `${appOrigin}/actions`
24
+ let actionsUrl = resolveActionsUrl(normalizedApi)
25
+ if (appOrigin && actionsUrl.startsWith('/')) {
26
+ actionsUrl = `${appOrigin}${actionsUrl}`
30
27
  }
31
28
 
32
29
  const mcpUrl = appOrigin ? `${appOrigin}/mcp` : '/mcp'
@@ -0,0 +1,24 @@
1
+ const STORAGE_KEY = 'ossy-visitor-id'
2
+
3
+ /**
4
+ * Stable anonymous visitor id for page-view ingest (unique visitors).
5
+ * Returns undefined when storage is unavailable (SSR, blocked cookies).
6
+ *
7
+ * @param {{ getItem?: Function, setItem?: Function } | null} [storage]
8
+ * @returns {string | undefined}
9
+ */
10
+ export function getOrCreateVisitorId (storage) {
11
+ const store = storage ?? (typeof globalThis !== 'undefined' ? globalThis.localStorage : null)
12
+ if (!store?.getItem || !store?.setItem) return undefined
13
+ try {
14
+ const existing = store.getItem(STORAGE_KEY)
15
+ if (existing) return existing
16
+ const id = typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
17
+ ? crypto.randomUUID()
18
+ : `${Date.now()}-${Math.random().toString(36).slice(2)}`
19
+ store.setItem(STORAGE_KEY, id)
20
+ return id
21
+ } catch {
22
+ return undefined
23
+ }
24
+ }