PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.9
Yatra – Travel Booking & Tour Operator Software v3.0.2.9
3.0.17 3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 All 85 releases
yatra / resources / js / pages / account / utils / downloads.ts

downloads.ts in Yatra – Travel Booking & Tour Operator Software 3.0.2.9, at resources/js/pages/account/utils/downloads.ts

400 lines 11.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Account PDFs use Yatra REST routes with cookie auth + X-WP-Nonce.
3 * Plain-permalink sites use index.php?rest_route=/yatra/v1 — query params must
4 * stay on the URL, not inside rest_route.
5 */
6
7 type RestConfig = { base: string; nonce: string };
8
9 function getAccountRestConfig(): RestConfig {
10 if (typeof window === "undefined") {
11 return { base: "/wp-json/yatra/v1", nonce: "" };
12 }
13 const w = window as unknown as {
14 yatraAccountPage?: { apiUrl?: string; nonce?: string };
15 yatraAdmin?: { apiUrl?: string; nonce?: string };
16 };
17 const raw =
18 w.yatraAccountPage?.apiUrl || w.yatraAdmin?.apiUrl || "/wp-json/yatra/v1";
19 const base = String(raw).replace(/\/$/, "");
20 const nonce = w.yatraAccountPage?.nonce || w.yatraAdmin?.nonce || "";
21 return { base, nonce };
22 }
23
24 function parseYatraRestSubpath(href: string): string {
25 const origin =
26 typeof window !== "undefined" ? window.location.origin : "http://localhost";
27 try {
28 const u = new URL(href, origin);
29 const rr = u.searchParams.get("rest_route");
30 if (rr) {
31 const decoded = decodeURIComponent(rr.replace(/\+/g, " "));
32 const idx = decoded.indexOf("/yatra/v1");
33 if (idx !== -1) {
34 return decoded.slice(idx + "/yatra/v1".length) || decoded;
35 }
36 return decoded;
37 }
38 const path = u.pathname || "";
39 const marker = "/yatra/v1";
40 const pos = path.indexOf(marker);
41 if (pos !== -1) {
42 return path.slice(pos + marker.length);
43 }
44 } catch {
45 /* ignore */
46 }
47 return "";
48 }
49
50 function parsePaymentIdFromHref(href: string): number | null {
51 const sub = parseYatraRestSubpath(href);
52 const m = sub.match(/^\/payment\/(\d+)\/invoice(?:\/?|$)/i);
53 return m ? parseInt(m[1], 10) : null;
54 }
55
56 function parseBookingDocFromHref(
57 href: string,
58 kind: "voucher" | "itinerary",
59 ): number | null {
60 const sub = parseYatraRestSubpath(href);
61 const re = new RegExp(`^/bookings/(\\d+)/${kind}(?:/?|$)`, "i");
62 const m = sub.match(re);
63 return m ? parseInt(m[1], 10) : null;
64 }
65
66 /**
67 * Build GET URL for booking PDF endpoints.
68 */
69 function buildBookingDocumentUrl(
70 baseRaw: string,
71 bookingId: number,
72 kind: "voucher" | "itinerary",
73 mode: "download" | "preview" = "download",
74 ): string {
75 const suffix = `/bookings/${bookingId}/${kind}`;
76 const origin =
77 typeof window !== "undefined" ? window.location.origin : "http://localhost";
78
79 let u: URL;
80 try {
81 u = new URL(String(baseRaw).trim(), origin);
82 } catch {
83 const b = String(baseRaw).replace(/\/$/, "");
84 const q = mode === "download" ? "?download=1" : "?preview=1";
85 return `${b}${suffix}${q}`;
86 }
87
88 if (u.searchParams.has("rest_route")) {
89 const route = (u.searchParams.get("rest_route") || "").replace(/\/$/, "");
90 u.searchParams.set("rest_route", `${route}${suffix}`);
91 if (mode === "download") {
92 u.searchParams.set("download", "1");
93 u.searchParams.delete("preview");
94 } else {
95 u.searchParams.set("preview", "1");
96 u.searchParams.delete("download");
97 }
98 return u.toString();
99 }
100
101 u.pathname = (u.pathname || "").replace(/\/$/, "") + suffix;
102 if (mode === "download") {
103 u.searchParams.set("download", "1");
104 u.searchParams.delete("preview");
105 } else {
106 u.searchParams.set("preview", "1");
107 u.searchParams.delete("download");
108 }
109 return u.toString();
110 }
111
112 function buildPaymentInvoiceUrl(
113 baseRaw: string,
114 paymentId: number,
115 mode: "download" | "preview",
116 ): string {
117 const suffix = `/payment/${paymentId}/invoice`;
118 const origin =
119 typeof window !== "undefined" ? window.location.origin : "http://localhost";
120
121 let u: URL;
122 try {
123 u = new URL(String(baseRaw).trim(), origin);
124 } catch {
125 const b = String(baseRaw).replace(/\/$/, "");
126 const q = mode === "download" ? "?download=1" : "?preview=1";
127 return `${b}${suffix}${q}`;
128 }
129
130 if (u.searchParams.has("rest_route")) {
131 const route = (u.searchParams.get("rest_route") || "").replace(/\/$/, "");
132 u.searchParams.set("rest_route", `${route}${suffix}`);
133 if (mode === "download") {
134 u.searchParams.set("download", "1");
135 u.searchParams.delete("preview");
136 } else {
137 u.searchParams.set("preview", "1");
138 u.searchParams.delete("download");
139 }
140 return u.toString();
141 }
142
143 u.pathname = (u.pathname || "").replace(/\/$/, "") + suffix;
144 if (mode === "download") {
145 u.searchParams.set("download", "1");
146 u.searchParams.delete("preview");
147 } else {
148 u.searchParams.set("preview", "1");
149 u.searchParams.delete("download");
150 }
151 return u.toString();
152 }
153
154 async function readFetchErrorMessage(res: Response): Promise<string> {
155 let msg = res.statusText || "Request failed";
156 try {
157 const j = await res.json();
158 if (j?.message) {
159 msg = typeof j.message === "string" ? j.message : msg;
160 }
161 } catch {
162 const t = await res.text().catch(() => "");
163 if (t && t.length < 200) {
164 msg = t;
165 }
166 }
167 return msg;
168 }
169
170 async function fetchPreviewPdf(url: string): Promise<Blob> {
171 const { nonce } = getAccountRestConfig();
172 if (!nonce) {
173 throw new Error("Missing REST nonce; reload the account page.");
174 }
175 const res = await fetch(url, {
176 method: "GET",
177 credentials: "include",
178 headers: {
179 "X-WP-Nonce": nonce,
180 Accept: "application/json",
181 },
182 });
183 if (!res.ok) {
184 throw new Error(await readFetchErrorMessage(res));
185 }
186 const data = (await res.json()) as {
187 pdf_data?: string;
188 filename?: string;
189 };
190 if (!data?.pdf_data || typeof data.pdf_data !== "string") {
191 throw new Error("Invalid preview response from server.");
192 }
193 const binary = atob(data.pdf_data);
194 const bytes = new Uint8Array(binary.length);
195 for (let i = 0; i < binary.length; i++) {
196 bytes[i] = binary.charCodeAt(i);
197 }
198 return new Blob([bytes], { type: "application/pdf" });
199 }
200
201 function openPdfInNewTab(blob: Blob): void {
202 const objectUrl = URL.createObjectURL(blob);
203 const win = window.open(objectUrl, "_blank", "noopener,noreferrer");
204 if (!win) {
205 URL.revokeObjectURL(objectUrl);
206 throw new Error(
207 "Popup blocked. Allow popups for this site to preview the PDF.",
208 );
209 }
210 window.setTimeout(() => URL.revokeObjectURL(objectUrl), 60_000);
211 }
212
213 async function downloadBookingBinary(
214 bookingId: number,
215 kind: "voucher" | "itinerary",
216 ): Promise<void> {
217 const { base, nonce } = getAccountRestConfig();
218 if (!nonce) {
219 throw new Error("Missing REST nonce; reload the account page.");
220 }
221 const url = buildBookingDocumentUrl(base, bookingId, kind, "download");
222 const res = await fetch(url, {
223 method: "GET",
224 credentials: "include",
225 headers: {
226 "X-WP-Nonce": nonce,
227 Accept: "application/pdf, application/octet-stream, */*",
228 },
229 });
230 if (!res.ok) {
231 throw new Error(await readFetchErrorMessage(res));
232 }
233 const blob = await res.blob();
234 const dispo = res.headers.get("Content-Disposition");
235 let filename = `${kind}-${bookingId}.pdf`;
236 if (dispo) {
237 const m = /filename\*?=(?:UTF-8'')?["']?([^";\n]+)/i.exec(dispo);
238 if (m?.[1]) {
239 try {
240 filename = decodeURIComponent(m[1].replace(/['"]/g, "").trim());
241 } catch {
242 filename = m[1].replace(/['"]/g, "").trim();
243 }
244 }
245 }
246 const objectUrl = URL.createObjectURL(blob);
247 const link = document.createElement("a");
248 link.href = objectUrl;
249 link.download = filename;
250 document.body.appendChild(link);
251 link.click();
252 document.body.removeChild(link);
253 URL.revokeObjectURL(objectUrl);
254 }
255
256 async function downloadPaymentInvoiceBinary(paymentId: number): Promise<void> {
257 const { base, nonce } = getAccountRestConfig();
258 if (!nonce) {
259 throw new Error("Missing REST nonce; reload the account page.");
260 }
261 const url = buildPaymentInvoiceUrl(base, paymentId, "download");
262 const res = await fetch(url, {
263 method: "GET",
264 credentials: "include",
265 headers: {
266 "X-WP-Nonce": nonce,
267 Accept: "application/pdf, application/octet-stream, */*",
268 },
269 });
270 if (!res.ok) {
271 throw new Error(await readFetchErrorMessage(res));
272 }
273 const blob = await res.blob();
274 const dispo = res.headers.get("Content-Disposition");
275 let filename = `invoice-${paymentId}.pdf`;
276 if (dispo) {
277 const m = /filename\*?=(?:UTF-8'')?["']?([^";\n]+)/i.exec(dispo);
278 if (m?.[1]) {
279 try {
280 filename = decodeURIComponent(m[1].replace(/['"]/g, "").trim());
281 } catch {
282 filename = m[1].replace(/['"]/g, "").trim();
283 }
284 }
285 }
286 const objectUrl = URL.createObjectURL(blob);
287 const link = document.createElement("a");
288 link.href = objectUrl;
289 link.download = filename;
290 document.body.appendChild(link);
291 link.click();
292 document.body.removeChild(link);
293 URL.revokeObjectURL(objectUrl);
294 }
295
296 export interface DocumentDownloadOptions {
297 bookingId?: number;
298 paymentId?: number;
299 documentType: "voucher" | "invoice" | "itinerary" | "all" | "downloads";
300 fallbackUrl?: string;
301 }
302
303 export const downloadDocument = async (
304 options: DocumentDownloadOptions,
305 ): Promise<void> => {
306 if (options.documentType === "downloads" && options.fallbackUrl) {
307 window.open(options.fallbackUrl, "_blank", "noopener,noreferrer");
308 return;
309 }
310
311 if (options.documentType === "invoice") {
312 const pid =
313 options.paymentId ?? parsePaymentIdFromHref(options.fallbackUrl || "");
314 if (pid) {
315 await downloadPaymentInvoiceBinary(pid);
316 return;
317 }
318 }
319
320 if (
321 options.documentType === "voucher" ||
322 options.documentType === "itinerary"
323 ) {
324 const kind = options.documentType === "voucher" ? "voucher" : "itinerary";
325 const bid =
326 options.bookingId ??
327 parseBookingDocFromHref(options.fallbackUrl || "", kind);
328 if (bid) {
329 await downloadBookingBinary(bid, kind);
330 return;
331 }
332 }
333
334 if (options.bookingId && options.documentType === "all") {
335 await downloadBookingBinary(options.bookingId, "voucher");
336 return;
337 }
338
339 console.error(`No handler for document type: ${options.documentType}`);
340 };
341
342 export const downloadVoucher = (bookingId: number) =>
343 downloadBookingBinary(bookingId, "voucher");
344
345 export const downloadInvoice = (paymentId: number) =>
346 downloadPaymentInvoiceBinary(paymentId);
347
348 export const downloadItinerary = (bookingId: number) =>
349 downloadBookingBinary(bookingId, "itinerary");
350
351 /** Open payment invoice PDF in a new tab (REST preview JSON → blob). */
352 export const previewPaymentInvoice = async (
353 paymentId: number,
354 ): Promise<void> => {
355 const { base, nonce } = getAccountRestConfig();
356 if (!nonce) {
357 throw new Error("Missing REST nonce; reload the account page.");
358 }
359 const url = buildPaymentInvoiceUrl(base, paymentId, "preview");
360 const blob = await fetchPreviewPdf(url);
361 openPdfInNewTab(blob);
362 };
363
364 export async function previewTravelDocument(doc: {
365 category: string;
366 url: string;
367 booking_id?: number;
368 payment_id?: number;
369 }): Promise<void> {
370 const { base, nonce } = getAccountRestConfig();
371 if (!nonce) {
372 throw new Error("Missing REST nonce; reload the account page.");
373 }
374
375 if (doc.category === "invoice") {
376 const pid = doc.payment_id ?? parsePaymentIdFromHref(doc.url);
377 if (!pid) {
378 throw new Error("Could not resolve invoice link.");
379 }
380 const url = buildPaymentInvoiceUrl(base, pid, "preview");
381 const blob = await fetchPreviewPdf(url);
382 openPdfInNewTab(blob);
383 return;
384 }
385
386 if (doc.category === "voucher" || doc.category === "itinerary") {
387 const kind = doc.category === "voucher" ? "voucher" : "itinerary";
388 const bid = doc.booking_id ?? parseBookingDocFromHref(doc.url, kind);
389 if (!bid) {
390 throw new Error("Could not resolve document link.");
391 }
392 const url = buildBookingDocumentUrl(base, bid, kind, "preview");
393 const blob = await fetchPreviewPdf(url);
394 openPdfInNewTab(blob);
395 return;
396 }
397
398 window.open(doc.url, "_blank", "noopener,noreferrer");
399 }
400