@autobusal/providers 1.40.5 → 1.40.7

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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.40.7 (2026-08-28)
4
+
5
+ - 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.
6
+
7
+ ## 1.40.6 (2026-08-26)
8
+
9
+ - settings types: addons gains `flex` and `seats` entries (both optional).
10
+
3
11
  ## 1.40.4
4
12
 
5
13
  ### Fixed
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,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@autobusal/providers",
3
- "version": "1.40.5",
3
+ "version": "1.40.7",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
6
  "main": "index.ts"
7
- }
7
+ }
package/types/settings.ts CHANGED
@@ -67,6 +67,20 @@ export interface SettingsData {
67
67
  * bought. Optional so an older payload cannot crash the checkout.
68
68
  */
69
69
  sms?: { available: boolean, price: number }
70
+
71
+ // Edited: Ferjolt Ozuni - Date: 2026-08-03 / typed 2026-08-26
72
+ // Flexible Ticket: `available` only - the real offer and price are
73
+ // per-booking (Orders\FlexController).
74
+ flex?: { available: boolean, price: number }
75
+
76
+ /**
77
+ * Edited: Claude - Date: 2026-08-26
78
+ * Seat selection. `available: true` is what flips checkout from the
79
+ * legacy required-free pick to optional-and-priced; `price` is per
80
+ * hand-picked seat, display only - the server prices the charge.
81
+ * Optional so an older payload cannot crash the checkout.
82
+ */
83
+ seats?: { available: boolean, price: number }
70
84
  }
71
85
 
72
86
  // Edited: Ferjolt Ozuni - Date: 2026-07-31