@autobusal/order-confirm 1.0.0 → 1.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,35 @@
1
+ # Changelog
2
+
3
+ All notable changes to this package are documented here. This project follows
4
+ [Keep a Changelog](https://keepachangelog.com/) and [Semantic Versioning](https://semver.org/).
5
+
6
+ ## [1.1.0] - 2026-07-25
7
+
8
+ ### Added
9
+
10
+ - **Post-purchase Telegram addon binding.** When a purchased order has a
11
+ Telegram notification addon still awaiting binding, the "purchased"
12
+ confirmation page now shows a "Connect Telegram" deep link (polls obtapi's
13
+ `GET /api/orders/telegram/status?hash=...` every 4s until bound). Mirrors
14
+ magus's account-level `AccountTelegram` binding flow, but order-scoped
15
+ since a guest checkout has no account to bind against.
16
+
17
+ ## [1.0.1] - 2026-07-19
18
+
19
+ ### Security
20
+
21
+ - `OrderConfirm.tsx`: fixed an HTML-injection (XSS) vector. The `hash` segment
22
+ comes straight from the URL (`/orders/:type/:hash`) and was interpolated into
23
+ markup rendered via `dangerouslySetInnerHTML`. Added an `escapeHtml` helper
24
+ that encodes `& < > " '` and applied it to the hash before interpolation
25
+ (`<strong>${ escapeHtml(String(hash ?? '')) }</strong>`), so a crafted hash
26
+ (e.g. containing `<img onerror=...>`) can no longer inject markup.
27
+
28
+ ### Fixed
29
+
30
+ - `OrderConfirm.tsx`: the "Manage order" action now routes based on auth state.
31
+ A logged-in user is sent to `/account/orders` (where the order is listed)
32
+ instead of the public `/viewtrip/:hash` lookup form that email-gates access to
33
+ their own order; guests still use the public `/viewtrip/:hash` lookup.
34
+
35
+ Authored by Ferjolt Ozuni. Consolidated from the magus and alvavel whitelabel patch sets into canonical @autobusal source (eliminates per-repo patch-package divergence).
package/OrderConfirm.tsx CHANGED
@@ -5,6 +5,7 @@ import { BiErrorAlt } from 'react-icons/bi';
5
5
  import { IoReturnUpBackOutline } from 'react-icons/io5';
6
6
  import { Meta, Button } from '@autobusal/common';
7
7
  import Reason from './Reason';
8
+ import Telegram from './Telegram/Telegram';
8
9
  import { Title, Actions } from './styles';
9
10
  import { useUserStore } from '@autobusal/providers/stores/user';
10
11
 
@@ -12,6 +13,15 @@ interface Props {
12
13
  t: TFunction<'common'>
13
14
  }
14
15
 
16
+ // hash comes straight from the URL (/orders/:type/:hash) and is interpolated into
17
+ // HTML that gets rendered via dangerouslySetInnerHTML below - escape it so a
18
+ // crafted hash segment (e.g. containing <img onerror=...>) can't inject markup.
19
+ const escapeHtml = (value: string): string => (
20
+ value.replace(/[&<>"']/g, char => ({
21
+ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', '\'': '&#39;'
22
+ }[char] as string))
23
+ );
24
+
15
25
  const OrderConfirm = ({ t }: Props): (JSX.Element | null) => {
16
26
  const [ searchParams ] = useSearchParams();
17
27
 
@@ -30,7 +40,10 @@ const OrderConfirm = ({ t }: Props): (JSX.Element | null) => {
30
40
  }
31
41
 
32
42
  const onManage = (): void => {
33
- navigate(`/viewtrip/${ hash }`);
43
+ // a logged-in user shouldn't be sent to the public order-lookup form (which
44
+ // email-gates access to their OWN order) - send them into their account
45
+ // orders where the order is listed. Guests use the public lookup.
46
+ navigate(UserData ? '/account/orders' : `/viewtrip/${ hash }`);
34
47
  };
35
48
 
36
49
  const onBack = (): void => {
@@ -45,11 +58,12 @@ const OrderConfirm = ({ t }: Props): (JSX.Element | null) => {
45
58
  </Title>
46
59
 
47
60
  <div className="box" dangerouslySetInnerHTML={{
48
- __html: t(`order_confirm.content.${ type}`, { order: `<strong>${ hash }</strong>` })
61
+ __html: t(`order_confirm.content.${ type}`, { order: `<strong>${ escapeHtml(String(hash ?? '')) }</strong>` })
49
62
  }} />
50
63
 
51
64
  <>
52
65
  { (type === 'cancelled' && message !== '') && <Reason message={ message } t={ t } /> }
66
+ { (type === 'purchased' && hash) && <Telegram hash={ hash } t={ t } /> }
53
67
  </>
54
68
 
55
69
  <Actions>
@@ -0,0 +1,43 @@
1
+ import { TFunction } from 'i18next';
2
+ import { Button } from '@autobusal/common';
3
+ import { useGetOrderTelegramStatus } from '../services';
4
+ import { Container } from './styles';
5
+
6
+ interface Props {
7
+ hash: string
8
+ t: TFunction<'common'>
9
+ }
10
+
11
+ // Edited: Ferjolt Ozuni - Date: 2026-07-25
12
+ // Mirrors magus's account-level AccountTelegram.tsx, but order-scoped: a
13
+ // guest checkout has no account to bind against, so this reflects the
14
+ // order_addons row's own pending-token/chat_id state instead (see
15
+ // obtapi's Orders\AddonsController::telegram()). Renders nothing if the
16
+ // Telegram addon wasn't purchased on this order, or once bound - the
17
+ // binding is a one-time step, not something to keep displaying.
18
+ const Telegram = ({ hash, t }: Props): (JSX.Element | null) => {
19
+ const { data, isFetching } = useGetOrderTelegramStatus(hash);
20
+
21
+ if (isFetching || !data?.purchased || data.bound) {
22
+ return null;
23
+ }
24
+
25
+ if (!data.link) {
26
+ return null;
27
+ }
28
+
29
+ return (
30
+ <Container className="box">
31
+ <p>{ t('order_confirm.telegram.instructions') }</p>
32
+
33
+ <a href={ data.link } target="_blank" rel="noreferrer">
34
+ <Button
35
+ type="button"
36
+ text={ t('order_confirm.telegram.connect') }
37
+ />
38
+ </a>
39
+ </Container>
40
+ );
41
+ };
42
+
43
+ export default Telegram;
@@ -0,0 +1,10 @@
1
+ import styled from 'styled-components';
2
+
3
+ export const Container = styled.div`
4
+ margin-top: 15px;
5
+ text-align: center;
6
+
7
+ p {
8
+ margin-bottom: 10px;
9
+ }
10
+ `;
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@autobusal/order-confirm",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
+ "author": "Ferjolt Ozuni",
4
5
  "type": "module",
5
6
  "main": "index.ts"
6
7
  }
package/services.ts ADDED
@@ -0,0 +1,37 @@
1
+ import { useQuery, UseQueryResult } from '@tanstack/react-query';
2
+ import { apiClient } from '@autobusal/providers';
3
+
4
+ export interface OrderTelegramStatus {
5
+ purchased: boolean
6
+ available?: boolean
7
+ bound?: boolean
8
+ link?: string
9
+ }
10
+
11
+ // Edited: Ferjolt Ozuni - Date: 2026-07-25
12
+ // Order-scoped equivalent of magus's Account/Telegram/services.ts
13
+ // useGetTelegramStatus - a guest checkout has no account to bind against,
14
+ // so the pending token/chat_id live on the order_addons row instead (see
15
+ // obtapi's Orders\AddonsController::telegram()).
16
+ export const useGetOrderTelegramStatus = (hash: string): UseQueryResult<OrderTelegramStatus> => (
17
+ useQuery({
18
+ queryKey: ['order-telegram-status', hash],
19
+ queryFn: async () => (
20
+ await apiClient
21
+ .get('/api/orders/telegram/status', {
22
+ params: { hash }
23
+ })
24
+ .then(response => (
25
+ response.data
26
+ ))
27
+ ),
28
+ // Poll while unbound, so "bound" flips true right after the buyer taps
29
+ // the deep link and messages the bot - no manual refresh needed.
30
+ // refetchIntervalInBackground is required: tapping the link backgrounds
31
+ // this tab (switching to the Telegram app/site), which is exactly when
32
+ // the poll needs to keep running - react-query pauses refetchInterval
33
+ // on a hidden tab by default.
34
+ refetchInterval: (query) => (query.state.data?.bound ? false : 4000),
35
+ refetchIntervalInBackground: true
36
+ })
37
+ );