@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 +66 -0
- package/Errors/Message.tsx +16 -3
- package/Errors/styles.ts +6 -0
- package/Notifications/Notifications.tsx +2 -0
- package/Providers.tsx +23 -13
- package/Queries/apiClient.ts +30 -11
- package/Setup/languages.ts +1 -1
- package/index.ts +2 -1
- package/package.json +2 -1
- package/types/settings.ts +29 -11
- package/types/users.ts +1 -0
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).
|
package/Errors/Message.tsx
CHANGED
|
@@ -19,15 +19,28 @@ const Message = ({ type }: Props): JSX.Element => {
|
|
|
19
19
|
|
|
20
20
|
const language = getLanguage('en');
|
|
21
21
|
|
|
22
|
-
|
|
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
|
-
|
|
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`
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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;
|
package/Queries/apiClient.ts
CHANGED
|
@@ -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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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;
|
package/Setup/languages.ts
CHANGED
|
@@ -20,7 +20,7 @@ const languages = (type: string, defaultLanguage: string): void => {
|
|
|
20
20
|
},
|
|
21
21
|
lng: getLanguage(defaultLanguage),
|
|
22
22
|
fallbackLng: defaultLanguage,
|
|
23
|
-
debug:
|
|
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
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
|
-
|
|
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 {
|