@autobusal/providers 1.2.7 → 1.2.9

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 ADDED
@@ -0,0 +1,66 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@autobusal/providers` are documented here. This project
4
+ adheres to [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [1.2.9] - 2026-07-19
8
+
9
+ ### Added
10
+
11
+ - **Selectable auth transport in `Queries/apiClient.ts`.** A new
12
+ `VITE_AUTH_MODE` env flag (`bff` | `bearer`, defaulting to `bff`) picks how
13
+ the Axios client talks to the API. In `bff` mode the client targets the
14
+ same-origin `/bff` with `withCredentials`, relying on the httpOnly
15
+ `bff_session` cookie so no Bearer token or CSRF token ever lives in
16
+ JavaScript. `bearer` mode preserves the legacy transport (`VITE_API_OBT`
17
+ base URL, `withXSRFToken`, localStorage token) for brands that cannot run a
18
+ server-side component at their origin.
19
+ - **`isBffMode` export** derived from `VITE_AUTH_MODE`, re-exported from the
20
+ package root (`index.ts`) so consumers can branch on the active transport.
21
+ - **`GatewayCredentials<T>` interface in `types/settings.ts`**, modelling the
22
+ new per-gateway shape of `{ mode, sandbox, production }` so every payment
23
+ gateway can store a sandbox and a production credential set plus which one
24
+ is active.
25
+
26
+ ### Changed
27
+
28
+ - **`Queries/apiClient.ts` request interceptor is now bearer-only.** The
29
+ interceptor that reads `token` from localStorage and sets the
30
+ `Authorization` header is only registered when not in BFF mode; in BFF mode
31
+ there is no token in JavaScript to attach.
32
+ - **`types/settings.ts` `LabelPayments` gateways reshaped.** `bkt`, `stripe`,
33
+ `mollie`, `raiffeisen`, and `nexi` are now wrapped in
34
+ `GatewayCredentials<...>` (sandbox/production/mode). The BKT credential key
35
+ was corrected from `authid` to `authId`, Stripe gained a `webhook_secret`
36
+ field, and `devpos` gained `mode`, `sandbox_url`, and `production_url` so
37
+ its API host is configurable per-label instead of hardcoded to the demo URL.
38
+ - **`Providers.tsx` wraps the tree in an outer `<Suspense>` boundary.**
39
+ `Styles` and `Setup` both call `useGetSettings()` (a suspense query) above
40
+ the existing inner boundary; on a cold settings cache their suspension had
41
+ no boundary to catch it. The outer boundary shows the `loading` fallback
42
+ during the initial settings fetch; the module-scope QueryClient means the
43
+ suspended remount reuses the same cache with no refetch loop.
44
+ - **`Notifications/Notifications.tsx` error toast duration raised to 6000ms**
45
+ (from the 4s default) so users are less likely to miss error notifications.
46
+ - **`Setup/languages.ts` i18next `debug` is now `import.meta.env.DEV`** instead
47
+ of hardcoded `true`, silencing i18next debug logging in production builds.
48
+ - **`Errors/Message.tsx` logo is now a keyboard-accessible control** (`role`,
49
+ `tabIndex`, `onKeyDown` for Enter/Space, `alt` text) with a matching
50
+ `:focus-visible` outline and `display: inline-block` in `Errors/styles.ts`.
51
+
52
+ ### Fixed
53
+
54
+ - **Error page crash on non-English locales (`Errors/Message.tsx`).** The
55
+ translations lookup now falls back to `errors.en` when the selected locale
56
+ is absent from `errors.ts`; previously a non-English selection (e.g.
57
+ `localStorage` language `sq`) made the lookup `undefined` and crashed the
58
+ error page.
59
+ - **Crash inside the 500 response interceptor (`Queries/apiClient.ts`).** The
60
+ server-error message lookup now falls back to `languages.en` for the same
61
+ reason, so a missing locale can no longer throw from within the error
62
+ interceptor itself.
63
+
64
+ Authored by Ferjolt Ozuni. Consolidated from the magus and alvavel whitelabel
65
+ patch sets into canonical @autobusal source (eliminates per-repo patch-package
66
+ divergence).
@@ -19,15 +19,28 @@ const Message = ({ type }: Props): JSX.Element => {
19
19
 
20
20
  const language = getLanguage('en');
21
21
 
22
- const translations = errors[language as keyof typeof errors];
22
+ // Fall back to English for any locale not present in errors.ts - otherwise a
23
+ // non-English selection (e.g. localStorage language = 'sq') would make this
24
+ // `undefined` and crash the error page on `.not_found`/`.system`.
25
+ const translations = errors[language as keyof typeof errors] ?? errors.en;
23
26
 
24
27
  return (
25
28
  <>
26
29
  <Background />
27
30
 
28
31
  <Container className="box">
29
- <Logo>
30
- <img src={ `${ data.url }/logo-${ theme }.svg` } onClick={ () => navigate('/') } />
32
+ <Logo
33
+ role="button"
34
+ tabIndex={ 0 }
35
+ onClick={ () => navigate('/') }
36
+ onKeyDown={ event => {
37
+ if (event.key === 'Enter' || event.key === ' ') {
38
+ event.preventDefault();
39
+ navigate('/');
40
+ }
41
+ } }
42
+ >
43
+ <img src={ `${ data.url }/logo-${ theme }.svg` } alt="Home" />
31
44
  </Logo>
32
45
 
33
46
  <Description>
package/Errors/styles.ts CHANGED
@@ -25,12 +25,18 @@ export const Container = styled.div`
25
25
 
26
26
  export const Logo = styled.div`
27
27
  margin-bottom: 20px;
28
+ display: inline-block;
28
29
 
29
30
  & > img {
30
31
  display: block;
31
32
  width: 250px;
32
33
  cursor: pointer;
33
34
  }
35
+
36
+ &:focus-visible {
37
+ outline: 2px solid currentColor;
38
+ outline-offset: 2px;
39
+ }
34
40
  `;
35
41
 
36
42
  export const Description = styled.div`
@@ -39,6 +39,8 @@ export const Provider = ({ children }: Props): JSX.Element => {
39
39
  },
40
40
 
41
41
  error: {
42
+ // errors stay up longer - at the default 4s users often miss them
43
+ duration: 6000,
42
44
  style: {
43
45
  background: ourTheme.font.error,
44
46
  color: '#FFFFFF'
package/Providers.tsx CHANGED
@@ -12,19 +12,29 @@ interface Props {
12
12
  }
13
13
 
14
14
  const Providers = ({ type, loading, children }: Props): JSX.Element => (
15
- <Recaptcha>
16
- <Queries>
17
- <Styles>
18
- <Notifications>
19
- <Setup type={ type }>
20
- <Suspense fallback={ loading }>
21
- { children }
22
- </Suspense>
23
- </Setup>
24
- </Notifications>
25
- </Styles>
26
- </Queries>
27
- </Recaptcha>
15
+ // Styles and Setup both call useGetSettings() (a useSuspenseQuery) and sit
16
+ // ABOVE the inner <Suspense> below, so on a cold settings cache their
17
+ // suspension had no boundary to catch it and React threw error #426
18
+ // ("a component suspended while responding to synchronous input"). This
19
+ // outer boundary catches those two suspensions (showing `loading` during
20
+ // the initial settings fetch). The QueryClient in <Queries> is created at
21
+ // module scope, so remounting this subtree while suspended keeps the same
22
+ // client and cache - no refetch loop.
23
+ <Suspense fallback={ loading }>
24
+ <Recaptcha>
25
+ <Queries>
26
+ <Styles>
27
+ <Notifications>
28
+ <Setup type={ type }>
29
+ <Suspense fallback={ loading }>
30
+ { children }
31
+ </Suspense>
32
+ </Setup>
33
+ </Notifications>
34
+ </Styles>
35
+ </Queries>
36
+ </Recaptcha>
37
+ </Suspense>
28
38
  );
29
39
 
30
40
  export default Providers;
@@ -3,14 +3,30 @@ import languages from '@lang/errors';
3
3
  import { getLanguage } from '../Setup/languages';
4
4
  import { error as systemError } from '@autobusal/utilities';
5
5
 
6
- const apiClient = axios.create({
7
- baseURL: import.meta.env.VITE_API_OBT,
8
- withCredentials: true,
9
- withXSRFToken: true
10
- });
6
+ // Auth transport is selectable per brand via VITE_AUTH_MODE - ONE codebase,
7
+ // one flag, so there is a single auth path to maintain across every whitelabel:
8
+ // - 'bff' (default): all calls go through the same-origin BFF (public/bff/,
9
+ // served at /bff), which holds the auth token server-side and attaches it
10
+ // upstream. The browser carries only a first-party httpOnly `bff_session`
11
+ // cookie, so there is no Bearer token in JavaScript and no CSRF to manage.
12
+ // - 'bearer': legacy transport for a brand that cannot run a server-side
13
+ // component at its origin (pure-static hosting) or hasn't deployed its BFF
14
+ // yet - a Sanctum token from localStorage is sent as `Authorization`.
15
+ // See AUTH_STORAGE_DESIGN.md.
16
+ const authMode = import.meta.env.VITE_AUTH_MODE ?? 'bff';
17
+
18
+ export const isBffMode = authMode !== 'bearer';
19
+
20
+ const apiClient = axios.create(
21
+ isBffMode
22
+ ? { baseURL: '/bff', withCredentials: true }
23
+ : { baseURL: import.meta.env.VITE_API_OBT, withCredentials: true, withXSRFToken: true }
24
+ );
11
25
 
12
- apiClient.interceptors.request.use(
13
- config => {
26
+ // bearer mode only: attach the token from localStorage. In bff mode the token
27
+ // never enters JavaScript, so there is nothing to attach here.
28
+ if (!isBffMode) {
29
+ apiClient.interceptors.request.use(config => {
14
30
  const token = localStorage.getItem('token');
15
31
 
16
32
  if (token !== null) {
@@ -18,8 +34,8 @@ apiClient.interceptors.request.use(
18
34
  }
19
35
 
20
36
  return config;
21
- }
22
- );
37
+ });
38
+ }
23
39
 
24
40
  apiClient.interceptors.response.use(response => (
25
41
  response
@@ -44,7 +60,10 @@ apiClient.interceptors.response.use(response => (
44
60
  if (error.response.status === 500) {
45
61
  const language = getLanguage('en');
46
62
 
47
- systemError(languages[language as keyof typeof languages].server);
63
+ // Fall back to English for any locale not present in errors.ts, so a
64
+ // non-English selection can't make this `undefined` and throw inside the
65
+ // error interceptor itself.
66
+ systemError((languages[language as keyof typeof languages] ?? languages.en).server);
48
67
  }
49
68
  } else if (error.request) {
50
69
  console.log('REQUEST IS NOT DEFINED');
@@ -55,4 +74,4 @@ apiClient.interceptors.response.use(response => (
55
74
  return Promise.reject(error);
56
75
  });
57
76
 
58
- export default apiClient;
77
+ export default apiClient;
@@ -20,7 +20,7 @@ const languages = (type: string, defaultLanguage: string): void => {
20
20
  },
21
21
  lng: getLanguage(defaultLanguage),
22
22
  fallbackLng: defaultLanguage,
23
- debug: true,
23
+ debug: import.meta.env.DEV,
24
24
  backend: {
25
25
  loadPath: `${ import.meta.env.VITE_API_OBT }/languages/{{lng}}/{{ns}}.json`
26
26
  },
package/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import Providers from './Providers';
2
- import apiClient from './Queries/apiClient';
2
+ import apiClient, { isBffMode } from './Queries/apiClient';
3
3
  import ErrorBoundary from './Errors/ErrorBoundary';
4
4
  import Message from './Errors/Message';
5
5
  import useMenuStore from './stores/menu';
@@ -7,6 +7,7 @@ import { useUserStore } from './stores/user';
7
7
 
8
8
  export {
9
9
  apiClient,
10
+ isBffMode,
10
11
  ErrorBoundary,
11
12
  Message,
12
13
  useMenuStore,
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@autobusal/providers",
3
- "version": "1.2.7",
3
+ "version": "1.2.9",
4
+ "author": "Ferjolt Ozuni",
4
5
  "type": "module",
5
6
  "main": "index.ts"
6
7
  }
package/types/settings.ts CHANGED
@@ -124,6 +124,16 @@ export interface LabelSettings {
124
124
  }
125
125
  }
126
126
 
127
+ // Edited: Ferjolt Ozuni - Date: 2026-07-18
128
+ // Added: every gateway now stores a sandbox AND a production credential
129
+ // set plus which one is active, instead of one flat set - see
130
+ // obtapi's app/Traits/PaymentCredentials.php for the backend side.
131
+ export interface GatewayCredentials<T> {
132
+ mode: 'sandbox' | 'production'
133
+ sandbox: T
134
+ production: T
135
+ }
136
+
127
137
  export interface LabelPayments {
128
138
  available: {
129
139
  reserve: boolean
@@ -135,40 +145,48 @@ export interface LabelPayments {
135
145
  nexi: boolean
136
146
  }
137
147
 
148
+ // Edited: Ferjolt Ozuni - Date: 2026-07-18
149
+ // Added: mode/sandbox_url/production_url - the DevPos API host was
150
+ // hardcoded to the demo url in obtapi's config/devpos.php, now
151
+ // configurable per-label - see App\Processes\DevPos\Client::endpoints().
138
152
  devpos: {
139
153
  vat: number
140
154
  barcode: string
141
155
  authorization: string
142
156
  exchange_rate: number
143
157
  cash_register_index: string
158
+ mode: 'sandbox' | 'production'
159
+ sandbox_url: string
160
+ production_url: string
144
161
  }
145
162
 
146
- bkt: {
163
+ bkt: GatewayCredentials<{
147
164
  url: string
148
- authid: string
165
+ authId: string
149
166
  domain: string
150
167
  storeKey: string
151
168
  currencyId: string
152
- }
169
+ }>
153
170
 
154
- stripe: {
171
+ stripe: GatewayCredentials<{
155
172
  account: string
156
173
  secret: string
157
- }
174
+ webhook_secret: string
175
+ }>
158
176
 
159
- mollie: {
177
+ mollie: GatewayCredentials<{
160
178
  key: string
161
- }
179
+ }>
162
180
 
163
- raiffeisen: {
181
+ raiffeisen: GatewayCredentials<{
164
182
  client_id: string
165
183
  username: string
166
184
  password: string
167
- }
185
+ }>
168
186
 
169
- nexi: {
187
+ nexi: GatewayCredentials<{
170
188
  api_key: string
171
- }
189
+ }>
172
190
  }
173
191
 
174
192
  export interface UploadData {
package/types/users.ts CHANGED
@@ -19,6 +19,7 @@ export interface UserData {
19
19
  subscription: SubscribedData
20
20
  api: ApiData
21
21
  created_at: string
22
+ token?: string
22
23
  admin?: AdminData
23
24
  financer?: FinancerData
24
25
  operator?: OperatorData