@autobusal/order-confirm 1.2.1 → 1.3.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/OrderConfirm.tsx CHANGED
@@ -4,12 +4,12 @@ import { useSearchParams, useParams, useNavigate, Navigate } from 'react-router-
4
4
  import { AiOutlineCheck } from 'react-icons/ai';
5
5
  import { BiErrorAlt } from 'react-icons/bi';
6
6
  import { IoReturnUpBackOutline } from 'react-icons/io5';
7
- import { Meta, Button } from '@autobusal/common';
7
+ import { Meta, Button, LoadingParagraph } from '@autobusal/common';
8
8
  import Reason from './Reason';
9
9
  import Telegram from './Telegram/Telegram';
10
10
  import { Title, Actions } from './styles';
11
11
  import { useUserStore } from '@autobusal/providers/stores/user';
12
- import { useReleaseOrder } from './services';
12
+ import { useReleaseOrder, useGetOrderNumber } from './services';
13
13
  import { flush } from '@autobusal/providers/Setup/commerce';
14
14
 
15
15
  interface Props {
@@ -25,6 +25,21 @@ const escapeHtml = (value: string): string => (
25
25
  }[char] as string))
26
26
  );
27
27
 
28
+ /*
29
+ * What stands in for the confirmation sentence until the booking reference
30
+ * has been fetched.
31
+ *
32
+ * The page's own <Title> already says "reserved" / "purchased", so the
33
+ * outcome is on screen the whole time - only the reference is waited for,
34
+ * and the two lines sit in the same `.box` the sentence will land in, so
35
+ * nothing shifts when it arrives.
36
+ */
37
+ const Waiting = (): JSX.Element => (
38
+ <div className="box">
39
+ <LoadingParagraph count={ 2 } />
40
+ </div>
41
+ );
42
+
28
43
  // long enough to read the confirmation and its order number, short enough
29
44
  // that nobody wonders whether the page has finished
30
45
  const REDIRECT_AFTER = 5000;
@@ -40,6 +55,24 @@ const OrderConfirm = ({ t }: Props): (JSX.Element | null) => {
40
55
 
41
56
  const { data: UserData } = useUserStore();
42
57
 
58
+ const { data: NumberData, isPending: numberPending } = useGetOrderNumber(hash);
59
+
60
+ /*
61
+ * Claude - 2026-08-31 (ruled by Ferjolt: "We should not show the internal
62
+ * hash anywhere but should show always the PNR").
63
+ *
64
+ * This used to fall back to `hash` when the lookup had not answered, which
65
+ * is how a live buyer came to see a 15-character Hashids token where the
66
+ * six-character booking reference belonged: the lookup was rate-limited
67
+ * during the "Too Many Attempts" episode, the fallback fired, and the page
68
+ * confidently printed an internal credential the visitor cannot type into
69
+ * "check your reservation" and cannot quote to anyone.
70
+ *
71
+ * The hash is no longer a fallback at all. It is not a booking reference,
72
+ * so it is never the answer to "what is my booking reference".
73
+ */
74
+ const orderNumber = NumberData?.order_number ?? '';
75
+
43
76
  /*
44
77
  * Claude - 2026-08-22 (audit finding BOOK-11-b): this page is where the
45
78
  * gateway's cancelUrl AND failUrl land, and it used to say "your order
@@ -131,9 +164,24 @@ const OrderConfirm = ({ t }: Props): (JSX.Element | null) => {
131
164
  { t(`order_confirm.title.${ type }`) }
132
165
  </Title>
133
166
 
134
- <div className="box" dangerouslySetInnerHTML={{
135
- __html: t(`order_confirm.content.${ type === 'cancelled' && released ? 'released' : type }`, { order: `<strong>${ escapeHtml(String(hash ?? '')) }</strong>` })
136
- }} />
167
+ { /* Claude - 2026-08-29 (audit A3-03h, low): the booking's REAL order
168
+ number, not the URL hash. This page announced the 15-character
169
+ Hashids token as "your order with number ..." while the email, the
170
+ ticket and the invoice all named the six-character PNR - and the
171
+ PNR is what /viewtrip's own Order Number field accepts and what a
172
+ support desk can look up.
173
+
174
+ 2026-08-31: the sentence waits for the number rather than filling
175
+ the gap with the hash. It is one short GET on a page that then sits
176
+ for five seconds before redirecting, so the wait is not felt - and
177
+ announcing the wrong reference is worse than announcing it a moment
178
+ later. `order` is bolded because the visitor is expected to WRITE
179
+ IT DOWN; it is the only thing on the page they need to keep. */ }
180
+ { numberPending ? <Waiting /> : (
181
+ <div className="box" dangerouslySetInnerHTML={{
182
+ __html: t(`order_confirm.content.${ type === 'cancelled' && released ? 'released' : type }`, { order: `<strong>${ escapeHtml(orderNumber) }</strong>` })
183
+ }} />
184
+ ) }
137
185
 
138
186
  <>
139
187
  { (type === 'cancelled' && message !== '') && <Reason message={ message } t={ t } /> }
package/package.json CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "name": "@autobusal/order-confirm",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
- "main": "index.ts"
6
+ "main": "index.ts",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ }
7
10
  }
package/services.ts CHANGED
@@ -36,6 +36,42 @@ export const useGetOrderTelegramStatus = (hash: string): UseQueryResult<OrderTel
36
36
  })
37
37
  );
38
38
 
39
+ /**
40
+ * The booking's real Order Number, from the hash this page was given.
41
+ *
42
+ * Claude - 2026-08-29 (audit A3-03h, low). The confirmation had only the URL
43
+ * hash and printed it as "your order with number gnl3d5kolxpw78q", while the
44
+ * email, the ticket and the invoice for the same booking all said ZRQGV2 -
45
+ * and ZRQGV2 is the code the /viewtrip lookup and a support desk actually
46
+ * accept. Not retried and allowed to fail: a confirmation page must never
47
+ * fail to confirm, so the caller falls back to the hash.
48
+ */
49
+ export const useGetOrderNumber = (hash?: string): UseQueryResult<{ order_number: string }> => (
50
+ useQuery({
51
+ queryKey: ['order-number', hash],
52
+ enabled: hash !== undefined && hash !== '',
53
+
54
+ /*
55
+ * Claude - 2026-08-31: this is the one query on the page that MUST answer.
56
+ * It was `retry: false`, and a single rate-limited response was enough to
57
+ * lose the buyer their booking reference on the page that exists to give
58
+ * it to them - which is how a live buyer was shown the internal hash.
59
+ *
60
+ * Retrying is safe here in a way it is not for the payment calls in the
61
+ * same file: this is a read, it creates nothing, and repeating it cannot
62
+ * duplicate an order the way a repeated initiation could.
63
+ */
64
+ retry: 3,
65
+ queryFn: async () => (
66
+ await apiClient
67
+ .get('/api/orders/number', { params: { hash } })
68
+ .then(response => (
69
+ response.data
70
+ ))
71
+ )
72
+ })
73
+ );
74
+
39
75
  /*
40
76
  * Claude - 2026-08-22 (audit finding BOOK-11-b): the "cancelled" return
41
77
  * page used to claim the order was cancelled while the reservation kept
package/CHANGELOG.md DELETED
@@ -1,76 +0,0 @@
1
- # Changelog
2
-
3
- ## 1.2.1
4
-
5
- ### Fixed
6
-
7
- - **The cancelled-payment page tells the truth.** It used to say "your order has been cancelled" while the reservation kept holding its seats for two more hours - and the gateway's failUrl lands on the same page, so auto-cancelling would kill declined-card retries. New copy (payment not completed, nothing charged, reservation still held), a Try-again action, and a real "Release my reservation" button through the hash-authorised cancel endpoint. (Audit BOOK-11-b.)
8
-
9
- ## 1.2.0
10
-
11
- - Announces `purchase` on arrival, for a **purchased** order only — a reservation is not revenue and a cancelled attempt certainly is not. Cleared as it fires, so refreshing this page (a plain URL with the buyer's order number on it) cannot report the same sale twice.
12
-
13
- All notable changes to this package are documented here. This project follows
14
- [Keep a Changelog](https://keepachangelog.com/) and [Semantic Versioning](https://semver.org/).
15
-
16
- ## [1.1.3] - 2026-08-01
17
-
18
- ### Changed
19
-
20
- - **The confirmation moves on to the order after five seconds**, where the
21
- tickets and the invoice actually are. A dead-end success page turned
22
- downloading the thing you had just bought into a hunt. Only for purchased
23
- and reserved: a cancellation has nothing to go to, and bouncing someone
24
- off a notice they may still be reading would be its own small insult.
25
- Cleared on unmount, so pressing a button first does not leave a second
26
- navigation queued behind the one you chose.
27
-
28
- ## [1.1.2] - 2026-07-30
29
-
30
- ### Fixed
31
-
32
- - **`noIndex` never used on the order confirmation page (`OrderConfirm.tsx`).**
33
- Reserved/purchased/cancelled order confirmations are reachable by anyone
34
- with the emailed link, no login required, and each is single-use - there is
35
- no canonical indexable version. `<Meta>`'s `noIndex` prop existed but was
36
- never passed anywhere in the app; it now is here.
37
-
38
- ## [1.1.1] - 2026-07-25
39
-
40
- ### Fixed
41
-
42
- - The "Connect Telegram" addon widget now also renders on `reserved` order
43
- confirmations, not just `purchased` ones - a reserve-then-pay-later
44
- booking has no other page offering the binding link, so it was a dead
45
- end for the Telegram addon on that path.
46
-
47
- ## [1.1.0] - 2026-07-25
48
-
49
- ### Added
50
-
51
- - **Post-purchase Telegram addon binding.** When a purchased order has a
52
- Telegram notification addon still awaiting binding, the "purchased"
53
- confirmation page now shows a "Connect Telegram" deep link (polls obtapi's
54
- `GET /api/orders/telegram/status?hash=...` every 4s until bound). Mirrors
55
- magus's account-level `AccountTelegram` binding flow, but order-scoped
56
- since a guest checkout has no account to bind against.
57
-
58
- ## [1.0.1] - 2026-07-19
59
-
60
- ### Security
61
-
62
- - `OrderConfirm.tsx`: fixed an HTML-injection (XSS) vector. The `hash` segment
63
- comes straight from the URL (`/orders/:type/:hash`) and was interpolated into
64
- markup rendered via `dangerouslySetInnerHTML`. Added an `escapeHtml` helper
65
- that encodes `& < > " '` and applied it to the hash before interpolation
66
- (`<strong>${ escapeHtml(String(hash ?? '')) }</strong>`), so a crafted hash
67
- (e.g. containing `<img onerror=...>`) can no longer inject markup.
68
-
69
- ### Fixed
70
-
71
- - `OrderConfirm.tsx`: the "Manage order" action now routes based on auth state.
72
- A logged-in user is sent to `/account/orders` (where the order is listed)
73
- instead of the public `/viewtrip/:hash` lookup form that email-gates access to
74
- their own order; guests still use the public `/viewtrip/:hash` lookup.
75
-
76
- Authored by Ferjolt Ozuni. Consolidated from the magus and alvavel whitelabel patch sets into canonical @autobusal source (eliminates per-repo patch-package divergence).