@autobusal/routes-order 1.6.0 → 1.6.2

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 CHANGED
@@ -3,6 +3,38 @@
3
3
  All notable changes to this package are documented here. This project follows
4
4
  [Keep a Changelog](https://keepachangelog.com/) and [Semantic Versioning](https://semver.org/).
5
5
 
6
+ ## [1.6.2] - 2026-07-30
7
+
8
+ ### Fixed
9
+
10
+ - **The full 9-segment `/bus-lines/:from/:to/:departure/:_return/:type/:adults/:children/:babies`
11
+ URL only auto-searched if a `?type=submit` query param was also present**
12
+ (`Step1/Step1.tsx`) - set only when the in-app search form navigated here
13
+ itself. Every OTHER way of reaching this URL - obtapi's
14
+ `Route::linkPurchase()` (which generates exactly this path with no query
15
+ param), a shared link, a search engine crawling it - rendered nothing but
16
+ the empty search form despite the URL already carrying a complete search
17
+ spec. Now also auto-searches whenever `:departure` is present in the path,
18
+ which only the full 9-segment route ever populates.
19
+
20
+ ## [1.6.1] - 2026-07-27
21
+
22
+ ### Added
23
+
24
+ - **External-provider "Book on {provider}" redirect (Mode A: search +
25
+ redirect).** `Found/Route/Route.tsx` now detects `data.external?.mode ===
26
+ 'redirect'` (obtapi's `Libraries\External\Search\Merge`) and swaps the
27
+ usual "Select Route" button for a `t('routes_order.step2.route.book_on')`
28
+ button that opens the click-out endpoint
29
+ (`{VITE_API_OBT}/api/external/go/{offer_id}`) in a new tab instead of
30
+ calling `onSave` - external offers use a synthetic id the local checkout
31
+ pipeline can't resolve, so "select" is never wired for them. The operator
32
+ logo and route-code links are also repointed to the same click-out URL
33
+ (not the raw provider deep link) so every outbound path is logged for the
34
+ search->click funnel, and the local-only "Read Operator Policy" button is
35
+ hidden for external rows (no local policy content exists for them).
36
+ Requires `@autobusal/providers` ^1.3.7.
37
+
6
38
  ## [1.6.0] - 2026-07-25
7
39
 
8
40
  ### Added
@@ -27,6 +27,17 @@ const Route = ({ data, type, t, onSave }: Props): JSX.Element => {
27
27
  <RouteFeature key={ index } item={ item } />
28
28
  ));
29
29
 
30
+ // Edited: Ferjolt Ozuni - Date: 2026-07-27
31
+ // Mode A (search + redirect): this row is an external-provider offer,
32
+ // not a local route - "select" would try to check out with a synthetic
33
+ // "ext:..." id, which the local pipeline can't resolve. Every outbound
34
+ // click goes through the click-out endpoint (not the raw deep link
35
+ // directly) so it's logged for the search->click funnel.
36
+ const isExternalRedirect = data.external?.mode === 'redirect';
37
+ const bookOnUrl = data.external
38
+ ? `${ import.meta.env.VITE_API_OBT }/api/external/go/${ data.external.offer_id }`
39
+ : undefined;
40
+
30
41
  return (
31
42
  <Container className="box" $type={ type }>
32
43
  { data.price.offer === true && <SpecialOffer>{ t('routes_order.step2.route.offer') }</SpecialOffer> }
@@ -34,7 +45,7 @@ const Route = ({ data, type, t, onSave }: Props): JSX.Element => {
34
45
 
35
46
  <Trip>
36
47
  <Company>
37
- <LinkCompany to={ data.operator.link } target="_blank" rel="noopener noreferrer" title={ data.operator.company }><img src={ data.operator.logo_url || LOGO_FALLBACK } onError={ (e) => { e.currentTarget.onerror = null; e.currentTarget.src = LOGO_FALLBACK; } } alt={ data.operator.company } /></LinkCompany>
48
+ <LinkCompany to={ bookOnUrl ?? data.operator.link } target="_blank" rel="noopener noreferrer" title={ data.operator.company }><img src={ data.operator.logo_url || LOGO_FALLBACK } onError={ (e) => { e.currentTarget.onerror = null; e.currentTarget.src = LOGO_FALLBACK; } } alt={ data.operator.company } /></LinkCompany>
38
49
  </Company>
39
50
 
40
51
  <Location>
@@ -62,10 +73,10 @@ const Route = ({ data, type, t, onSave }: Props): JSX.Element => {
62
73
  text={
63
74
  <>
64
75
  <RiBus2Line />
65
- { t('routes_order.step2.route.select') }
76
+ { isExternalRedirect ? t('routes_order.step2.route.book_on', { provider: data.operator.company }) : t('routes_order.step2.route.select') }
66
77
  </>
67
78
  }
68
- onClick={ () => onSave(data) }
79
+ onClick={ () => (isExternalRedirect && bookOnUrl ? window.open(bookOnUrl, '_blank', 'noopener,noreferrer') : onSave(data)) }
69
80
  />
70
81
  </Price>
71
82
  </Trip>
@@ -73,7 +84,7 @@ const Route = ({ data, type, t, onSave }: Props): JSX.Element => {
73
84
  <Details>
74
85
  <Detail>
75
86
  <TitleDetail>{ t('routes_order.step2.route.name') }</TitleDetail>
76
- <LinkRoute to={ data.link } target="_blank" rel="noopener noreferrer">{ data.code }</LinkRoute>
87
+ <LinkRoute to={ bookOnUrl ?? data.link } target="_blank" rel="noopener noreferrer">{ data.code }</LinkRoute>
77
88
  </Detail>
78
89
 
79
90
  { customs && (
@@ -90,14 +101,16 @@ const Route = ({ data, type, t, onSave }: Props): JSX.Element => {
90
101
  </Detail>
91
102
  ) }
92
103
 
93
- <Detail>
94
- <TitleDetail>{ t('routes_order.step2.route.policies.title') }</TitleDetail>
104
+ { !data.external && (
105
+ <Detail>
106
+ <TitleDetail>{ t('routes_order.step2.route.policies.title') }</TitleDetail>
95
107
 
96
- <ButtonPolicy type="button" onClick={ () => setPolicy(true) }>
97
- <HiOutlineDocumentText />
98
- { t('routes_order.step2.route.policies.view') }
99
- </ButtonPolicy>
100
- </Detail>
108
+ <ButtonPolicy type="button" onClick={ () => setPolicy(true) }>
109
+ <HiOutlineDocumentText />
110
+ { t('routes_order.step2.route.policies.view') }
111
+ </ButtonPolicy>
112
+ </Detail>
113
+ ) }
101
114
  </Details>
102
115
 
103
116
  { data.trip.features_data.length > 0 && (
@@ -110,7 +123,7 @@ const Route = ({ data, type, t, onSave }: Props): JSX.Element => {
110
123
  </Features>
111
124
  ) }
112
125
 
113
- { policy && <Policy id={ data.id } t={ t } onClose={ () => setPolicy(false) } /> }
126
+ { policy && typeof data.id === 'number' && <Policy id={ data.id } t={ t } onClose={ () => setPolicy(false) } /> }
114
127
  </Container>
115
128
  );
116
129
  };
package/Step1/Step1.tsx CHANGED
@@ -25,7 +25,17 @@ const Step1 = ({ value, redirected, t, onSave }: Props): JSX.Element => {
25
25
 
26
26
  const prepared = prepare(value, params, searchParams, data.preferences.defaultLocations);
27
27
 
28
- const isSubmitted = searchParams.get('type') === 'submit';
28
+ // Two ways this page ends up with a complete search spec:
29
+ // 1. `?type=submit` - set by the in-app search form (home/Main/Main.tsx)
30
+ // when it navigates here itself.
31
+ // 2. `:departure` present in the URL path - the full 9-segment route
32
+ // (/bus-lines/:from/:to/:departure/:_return/:type/:adults/:children/:babies)
33
+ // already carries a complete search spec with no query param needed.
34
+ // This is how obtapi's Route::linkPurchase() generates links, and how
35
+ // anyone (a search engine, a shared link) reaches this URL directly -
36
+ // previously only case 1 triggered a search, so every one of those
37
+ // deep links rendered nothing but the empty search form.
38
+ const isSubmitted = searchParams.get('type') === 'submit' || params.departure !== undefined;
29
39
 
30
40
  useEffect(() => {
31
41
  if (isSubmitted && !redirected) {
package/Step4/Step4.tsx CHANGED
@@ -40,8 +40,13 @@ const Step4 = ({ values, step1, step2, step3, isPartner, t, onSave, onBack }: Pr
40
40
  ...(step3 !== undefined ? (step3.trip?.passenger_fields ?? DEFAULT_FIELDS) : [])
41
41
  ]));
42
42
 
43
- const busFrom = useBusOccupied(step1.departure, step2.id);
44
- const busTo = useBusOccupied(step1._return, step3?.id);
43
+ // Edited: Ferjolt Ozuni - Date: 2026-07-27
44
+ // FoundData.id is widened to (number | string) for external-provider
45
+ // offers (Mode A: search + redirect), but those never reach Step4 -
46
+ // Found/Route.tsx's CTA opens the provider's click-out link for them
47
+ // instead of calling onSave, so a real local numeric id is guaranteed here.
48
+ const busFrom = useBusOccupied(step1.departure, step2.id as number);
49
+ const busTo = useBusOccupied(step1._return, step3?.id as (number | undefined));
45
50
 
46
51
  if (busFrom === undefined || busTo === undefined) {
47
52
  return <Loading t={ t } />;
@@ -21,7 +21,12 @@ interface Props {
21
21
  const Coupons = ({ current, step2, t, total, onApplied }: Props): (JSX.Element | null) => {
22
22
  const { register, handleSubmit, formState: { errors } } = useForm<CouponForm>();
23
23
 
24
- const { mutate: SearchCoupons, isPending } = useGetCoupon(step2.id, total.value);
24
+ // Edited: Ferjolt Ozuni - Date: 2026-07-27
25
+ // step2.id is only a synthetic string for external-provider offers
26
+ // (Mode A: search + redirect), and those never reach Step5/checkout -
27
+ // Found/Route.tsx's CTA redirects to the provider instead of calling
28
+ // onSave, so a real local numeric id is guaranteed here.
29
+ const { mutate: SearchCoupons, isPending } = useGetCoupon(step2.id as number, total.value);
25
30
 
26
31
  const onSubmit = (data: CouponForm): void => {
27
32
  SearchCoupons(data, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobusal/routes-order",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
6
  "main": "index.ts"