@autobusal/order-confirm 1.2.2 → 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,7 +4,7 @@ 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';
@@ -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,9 +55,23 @@ const OrderConfirm = ({ t }: Props): (JSX.Element | null) => {
40
55
 
41
56
  const { data: UserData } = useUserStore();
42
57
 
43
- const { data: NumberData } = useGetOrderNumber(hash);
58
+ const { data: NumberData, isPending: numberPending } = useGetOrderNumber(hash);
44
59
 
45
- const orderNumber = NumberData?.order_number ?? String(hash ?? '');
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 ?? '';
46
75
 
47
76
  /*
48
77
  * Claude - 2026-08-22 (audit finding BOOK-11-b): this page is where the
@@ -140,12 +169,19 @@ const OrderConfirm = ({ t }: Props): (JSX.Element | null) => {
140
169
  Hashids token as "your order with number ..." while the email, the
141
170
  ticket and the invoice all named the six-character PNR - and the
142
171
  PNR is what /viewtrip's own Order Number field accepts and what a
143
- support desk can look up. Falls back to the hash while the lookup
144
- is in flight or if it fails, which is exactly what was shown
145
- before, so a confirmation page can still always confirm. */ }
146
- <div className="box" dangerouslySetInnerHTML={{
147
- __html: t(`order_confirm.content.${ type === 'cancelled' && released ? 'released' : type }`, { order: `<strong>${ escapeHtml(orderNumber) }</strong>` })
148
- }} />
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
+ ) }
149
185
 
150
186
  <>
151
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.2",
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
@@ -50,7 +50,18 @@ export const useGetOrderNumber = (hash?: string): UseQueryResult<{ order_number:
50
50
  useQuery({
51
51
  queryKey: ['order-number', hash],
52
52
  enabled: hash !== undefined && hash !== '',
53
- retry: false,
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,
54
65
  queryFn: async () => (
55
66
  await apiClient
56
67
  .get('/api/orders/number', { params: { hash } })
package/CHANGELOG.md DELETED
@@ -1,80 +0,0 @@
1
- # Changelog
2
-
3
- ## 1.2.2 - 2026-08-29
4
-
5
- - The purchase confirmation shows the real PNR rather than the internal hash from its own URL (audit A3-03h) - the email, the ticket and the invoice all name the PNR, and it is the code /viewtrip and a support desk accept. Falls back to the hash, so the page can always confirm.
6
-
7
- ## 1.2.1
8
-
9
- ### Fixed
10
-
11
- - **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.)
12
-
13
- ## 1.2.0
14
-
15
- - 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.
16
-
17
- All notable changes to this package are documented here. This project follows
18
- [Keep a Changelog](https://keepachangelog.com/) and [Semantic Versioning](https://semver.org/).
19
-
20
- ## [1.1.3] - 2026-08-01
21
-
22
- ### Changed
23
-
24
- - **The confirmation moves on to the order after five seconds**, where the
25
- tickets and the invoice actually are. A dead-end success page turned
26
- downloading the thing you had just bought into a hunt. Only for purchased
27
- and reserved: a cancellation has nothing to go to, and bouncing someone
28
- off a notice they may still be reading would be its own small insult.
29
- Cleared on unmount, so pressing a button first does not leave a second
30
- navigation queued behind the one you chose.
31
-
32
- ## [1.1.2] - 2026-07-30
33
-
34
- ### Fixed
35
-
36
- - **`noIndex` never used on the order confirmation page (`OrderConfirm.tsx`).**
37
- Reserved/purchased/cancelled order confirmations are reachable by anyone
38
- with the emailed link, no login required, and each is single-use - there is
39
- no canonical indexable version. `<Meta>`'s `noIndex` prop existed but was
40
- never passed anywhere in the app; it now is here.
41
-
42
- ## [1.1.1] - 2026-07-25
43
-
44
- ### Fixed
45
-
46
- - The "Connect Telegram" addon widget now also renders on `reserved` order
47
- confirmations, not just `purchased` ones - a reserve-then-pay-later
48
- booking has no other page offering the binding link, so it was a dead
49
- end for the Telegram addon on that path.
50
-
51
- ## [1.1.0] - 2026-07-25
52
-
53
- ### Added
54
-
55
- - **Post-purchase Telegram addon binding.** When a purchased order has a
56
- Telegram notification addon still awaiting binding, the "purchased"
57
- confirmation page now shows a "Connect Telegram" deep link (polls obtapi's
58
- `GET /api/orders/telegram/status?hash=...` every 4s until bound). Mirrors
59
- magus's account-level `AccountTelegram` binding flow, but order-scoped
60
- since a guest checkout has no account to bind against.
61
-
62
- ## [1.0.1] - 2026-07-19
63
-
64
- ### Security
65
-
66
- - `OrderConfirm.tsx`: fixed an HTML-injection (XSS) vector. The `hash` segment
67
- comes straight from the URL (`/orders/:type/:hash`) and was interpolated into
68
- markup rendered via `dangerouslySetInnerHTML`. Added an `escapeHtml` helper
69
- that encodes `& < > " '` and applied it to the hash before interpolation
70
- (`<strong>${ escapeHtml(String(hash ?? '')) }</strong>`), so a crafted hash
71
- (e.g. containing `<img onerror=...>`) can no longer inject markup.
72
-
73
- ### Fixed
74
-
75
- - `OrderConfirm.tsx`: the "Manage order" action now routes based on auth state.
76
- A logged-in user is sent to `/account/orders` (where the order is listed)
77
- instead of the public `/viewtrip/:hash` lookup form that email-gates access to
78
- their own order; guests still use the public `/viewtrip/:hash` lookup.
79
-
80
- Authored by Ferjolt Ozuni. Consolidated from the magus and alvavel whitelabel patch sets into canonical @autobusal source (eliminates per-repo patch-package divergence).