@autobusal/providers 1.40.6 → 1.41.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/CHANGELOG.md +8 -0
- package/Setup/Preload.tsx +30 -8
- package/Setup/Setup.tsx +5 -0
- package/Setup/useReferralTouch.ts +97 -0
- package/index.ts +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.41.0 (2026-08-29)
|
|
4
|
+
|
|
5
|
+
- The app-shell title is set through document.title instead of a rendered <title> element - React 19 does not deduplicate titles across components, so the shell's and the page's both reached <head>, which is what Meta's cleanup was deleting one of (Sentry BUSMAGUS-7).
|
|
6
|
+
|
|
7
|
+
## 1.40.7 (2026-08-28)
|
|
8
|
+
|
|
9
|
+
- useReferralTouch: the web finally reads ?ref= - touch on landing, token in localStorage (referral_token field: the BFF strips top-level token); storedReferral() exported for checkout replay.
|
|
10
|
+
|
|
3
11
|
## 1.40.6 (2026-08-26)
|
|
4
12
|
|
|
5
13
|
- settings types: addons gains `flex` and `seats` entries (both optional).
|
package/Setup/Preload.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
1
2
|
import { JsonLd, setServerToday } from '@autobusal/common';
|
|
2
3
|
import { SettingsData } from '../types/settings';
|
|
3
4
|
import { useSystemTheme } from '@autobusal/hooks';
|
|
@@ -8,12 +9,28 @@ interface Props {
|
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
// React 19 automatically hoists any <title>/<meta>/<link> rendered anywhere
|
|
11
|
-
// in the tree into <head>
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
12
|
+
// in the tree into <head> - no wrapper component needed. Replaced
|
|
13
|
+
// react-helmet, which had gone silently non-functional under React 19 (see
|
|
14
|
+
// @autobusal/common's Meta.tsx for how this was diagnosed).
|
|
15
|
+
//
|
|
16
|
+
// Edited: Claude - Date: 2026-08-29 (Sentry BUSMAGUS-7)
|
|
17
|
+
//
|
|
18
|
+
// The shell title is set IMPERATIVELY, and is no longer a <title> element.
|
|
19
|
+
//
|
|
20
|
+
// It used to be `<title>{ company }</title>` rendered here, above the
|
|
21
|
+
// router, on the stated assumption that React 19 deduplicates titles and
|
|
22
|
+
// the page's own deeper <Meta title=...> would override it. It does not:
|
|
23
|
+
// both are hoisted and BOTH end up in <head>, which is why Meta's cleanup
|
|
24
|
+
// was written to keep one element and delete the others - and deleting the
|
|
25
|
+
// other one meant deleting a node React owned, leaving its fiber pointing
|
|
26
|
+
// at a detached element. The next render or unmount then called
|
|
27
|
+
// removeChild on a null parent: BUSMAGUS-7, an uncaught TypeError that
|
|
28
|
+
// aborts the navigation in flight.
|
|
29
|
+
//
|
|
30
|
+
// Setting document.title directly competes with nothing: a page's <Meta>
|
|
31
|
+
// renders the one and only <title> ELEMENT, and React sets the document
|
|
32
|
+
// title from it when it commits. The guard keeps this a FALLBACK - if a
|
|
33
|
+
// page has already put its own title up, the shell must not overwrite it.
|
|
17
34
|
const Preload = ({ data, children }: Props): JSX.Element => {
|
|
18
35
|
// Edited: Ferjolt Ozuni - Date: 2026-08-03
|
|
19
36
|
// Hand the booking calendar the SERVER's idea of today, before anything
|
|
@@ -23,6 +40,13 @@ const Preload = ({ data, children }: Props): JSX.Element => {
|
|
|
23
40
|
// sold them quite happily.
|
|
24
41
|
setServerToday(data.today);
|
|
25
42
|
|
|
43
|
+
// the app-shell title, for the moment before a page's own <Meta> mounts
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!document.head.querySelector('title')) {
|
|
46
|
+
document.title = data.preferences.company;
|
|
47
|
+
}
|
|
48
|
+
}, [ data.preferences.company ]);
|
|
49
|
+
|
|
26
50
|
// we arrange the font family names before inserting them
|
|
27
51
|
const fontTitle = data.theme.fontFamily.title.replace(/ /g, '+');
|
|
28
52
|
const fontContent = data.theme.fontFamily.content.replace(/ /g, '+');
|
|
@@ -67,8 +91,6 @@ const Preload = ({ data, children }: Props): JSX.Element => {
|
|
|
67
91
|
|
|
68
92
|
return (
|
|
69
93
|
<>
|
|
70
|
-
<title>{ data.preferences.company }</title>
|
|
71
|
-
|
|
72
94
|
<link rel="apple-touch-icon" sizes="180x180" href={ `${ data.url }/favicons/apple-touch-icon.png` } />
|
|
73
95
|
<link rel="icon" type="image/png" sizes="32x32" href={ `${ data.url }/favicons/favicon-32x32.png` } />
|
|
74
96
|
<link rel="icon" type="image/png" sizes="16x16" href={ `${ data.url }/favicons/favicon-16x16.png` } />
|
package/Setup/Setup.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import Preload from './Preload';
|
|
|
2
2
|
import languages from './languages';
|
|
3
3
|
import analytics from './analytics';
|
|
4
4
|
import useUserBootstrap from './useUserBootstrap';
|
|
5
|
+
import useReferralTouch from './useReferralTouch';
|
|
5
6
|
import { useGetSettings, useGetMenu } from '../services';
|
|
6
7
|
|
|
7
8
|
interface Props {
|
|
@@ -15,6 +16,10 @@ const Setup = ({ type, children }: Props): JSX.Element => {
|
|
|
15
16
|
useGetMenu();
|
|
16
17
|
useUserBootstrap();
|
|
17
18
|
|
|
19
|
+
// records ?ref= arrivals so checkout can attribute the affiliate - see
|
|
20
|
+
// the hook for why this is the web's only referral read
|
|
21
|
+
useReferralTouch();
|
|
22
|
+
|
|
18
23
|
languages(type, data.preferences.languages.default, data.preferences.languages.available);
|
|
19
24
|
|
|
20
25
|
// Edited: Ferjolt Ozuni - Date: 2026-08-01
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import apiClient from '../Queries/apiClient';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The web half of referral attribution (P4/P8).
|
|
6
|
+
*
|
|
7
|
+
* Edited: Claude - Date: 2026-08-28
|
|
8
|
+
*
|
|
9
|
+
* The backend has carried the whole pipeline since P4 - POST
|
|
10
|
+
* /api/referrals/touch mints a token, Orders\Make replays it as `referral`
|
|
11
|
+
* and Attribution stamps the order - but only the MOBILE app ever called
|
|
12
|
+
* it: nothing on the web read ?ref= at all, so a shared link or an
|
|
13
|
+
* embedded search widget (P8) could never pay its affiliate. This hook is
|
|
14
|
+
* that missing read.
|
|
15
|
+
*
|
|
16
|
+
* Fired once per app mount, and only when ?ref= is actually present -
|
|
17
|
+
* the endpoint writes a row and is throttled, so it must never become a
|
|
18
|
+
* page-view beacon (the same reasoning that kept it out of /settings/get
|
|
19
|
+
* server-side). A NEW code overwrites a stored touch: last shared link
|
|
20
|
+
* wins, which is the only order two competing affiliates would both
|
|
21
|
+
* recognise as fair.
|
|
22
|
+
*
|
|
23
|
+
* The token is stored, not the code: the token is what checkout replays
|
|
24
|
+
* (routes-order usePostOrder), and the server already knows everything
|
|
25
|
+
* else. localStorage survives the tab - a visitor who arrives from a
|
|
26
|
+
* partner site today and buys tomorrow still attributes, up to the
|
|
27
|
+
* server-declared expiry, which is stored alongside so the client never
|
|
28
|
+
* replays a token the server would ignore.
|
|
29
|
+
*
|
|
30
|
+
* An unknown or retired code answers 404 and stores nothing - a token the
|
|
31
|
+
* checkout cannot use is worse than none. Failures are silent: attribution
|
|
32
|
+
* is the affiliate's concern, never the buyer's problem.
|
|
33
|
+
*/
|
|
34
|
+
const STORAGE_KEY = 'referral';
|
|
35
|
+
|
|
36
|
+
export interface StoredReferral {
|
|
37
|
+
token: string
|
|
38
|
+
expires_at: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const storedReferral = (): (StoredReferral | null) => {
|
|
42
|
+
try {
|
|
43
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
44
|
+
|
|
45
|
+
if (raw === null) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const parsed = JSON.parse(raw) as StoredReferral;
|
|
50
|
+
|
|
51
|
+
if (!parsed.token || !parsed.expires_at || new Date(parsed.expires_at) <= new Date()) {
|
|
52
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
53
|
+
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return parsed;
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const useReferralTouch = (): void => {
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
const code = new URLSearchParams(window.location.search).get('ref');
|
|
66
|
+
|
|
67
|
+
if (code === null || code.trim() === '') {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
apiClient
|
|
72
|
+
.post('/api/referrals/touch', { code: code.trim() })
|
|
73
|
+
.then(response => {
|
|
74
|
+
// `referral_token` first: the BFF strips any top-level `token` from
|
|
75
|
+
// proxied JSON (it looks exactly like a leaked bearer), so the web
|
|
76
|
+
// reads the duplicate field the API added for it. Bearer-mode
|
|
77
|
+
// clients see both; `token` alone is the pre-rename mobile wire.
|
|
78
|
+
const data = response?.data ?? {};
|
|
79
|
+
const token = data.referral_token ?? data.token;
|
|
80
|
+
const expires_at = data.expires_at;
|
|
81
|
+
|
|
82
|
+
if (token && expires_at) {
|
|
83
|
+
try {
|
|
84
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify({ token, expires_at }));
|
|
85
|
+
} catch {
|
|
86
|
+
// storage unavailable - the visit still works, it just cannot attribute
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
.catch(() => {
|
|
91
|
+
// dead code, throttle, or offline - nothing to store, nothing to say
|
|
92
|
+
});
|
|
93
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
94
|
+
}, []);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export default useReferralTouch;
|
package/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Providers from './Providers';
|
|
2
2
|
import apiClient, { isBffMode } from './Queries/apiClient';
|
|
3
|
+
import { storedReferral } from './Setup/useReferralTouch';
|
|
3
4
|
import ErrorBoundary from './Errors/ErrorBoundary';
|
|
4
5
|
import Message from './Errors/Message';
|
|
5
6
|
import useMenuStore from './stores/menu';
|
|
@@ -7,6 +8,7 @@ import { useUserStore } from './stores/user';
|
|
|
7
8
|
import registerStaleChunkRecovery from './Setup/staleChunk';
|
|
8
9
|
|
|
9
10
|
export {
|
|
11
|
+
storedReferral,
|
|
10
12
|
apiClient,
|
|
11
13
|
isBffMode,
|
|
12
14
|
ErrorBoundary,
|