Skip to content

Commit 7e6efb3

Browse files
author
Lalit Sharma
committed
feat: update changelog for version 1.1.43, add fallback parser for Google Maps short links, and enhance test coverage for parsing
1 parent 9814871 commit 7e6efb3

4 files changed

Lines changed: 130 additions & 35 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.1.43] — 2026-02-27
9+
10+
### Fixed
11+
- Fixed shared Google Maps short links (`maps.app.goo.gl`) that expanded to place URLs without direct coordinate query/path params by adding a fallback parser for encoded preview payload coordinates.
12+
13+
### Added
14+
- Added regression test coverage for short-link parsing when coordinates are only present in Google preview payload data.
15+
16+
### Changed
17+
- Bumped `apps/mobile` version to `1.1.43`.
18+
819
## [1.1.42] — 2026-02-27
920

1021
### Fixed

apps/mobile/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@eclipse-timer/mobile",
3-
"version": "1.1.42",
3+
"version": "1.1.43",
44
"private": true,
55
"main": "index.js",
66
"scripts": {

apps/mobile/src/utils/sharedMapLink.ts

Lines changed: 99 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export type ParsedSharedMapLink = {
99

1010
type FetchResponseLike = {
1111
url?: string;
12+
text?: () => Promise<string>;
1213
};
1314

1415
type FetchLike = (input: string, init?: Record<string, unknown>) => Promise<FetchResponseLike>;
@@ -32,6 +33,15 @@ const GOOGLE_PATH_COORD_RE = new RegExp(
3233
`@(${NUMBER_PART})\\s*,\\s*(${NUMBER_PART})(?:\\s*,|\\s*$)`,
3334
"i",
3435
);
36+
const GOOGLE_PB_COORD_RE = new RegExp(
37+
`(?:%21|!)2d(${NUMBER_PART})(?:%21|!)3d(${NUMBER_PART})`,
38+
"i",
39+
);
40+
41+
type ExpandedShortMapUrlResult = {
42+
expandedUrl: string;
43+
responseText: string | null;
44+
};
3545

3646
function normalizeHost(hostname: string): string {
3747
return hostname.trim().replace(/\.+$/, "").toLowerCase();
@@ -137,6 +147,77 @@ function parseSharedMapLinkFromUrl(url: URL, rawUrl: string): ParsedSharedMapLin
137147
};
138148
}
139149

150+
function parseGooglePreviewCoordinatesFromText(input: string): { lat: number; lon: number } | null {
151+
if (!input) return null;
152+
153+
const match = input.match(GOOGLE_PB_COORD_RE);
154+
if (!match) return null;
155+
156+
const [, lonRaw = "", latRaw = ""] = match;
157+
const lon = Number(lonRaw);
158+
const lat = Number(latRaw);
159+
if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null;
160+
161+
return sanitizeCoordinates({ lat, lon });
162+
}
163+
164+
async function expandShortMapUrlWithResponse(
165+
url: string,
166+
options: ExpandShortMapUrlOptions = {},
167+
readResponseText = true,
168+
): Promise<ExpandedShortMapUrlResult> {
169+
const parsed = parseUrl(url);
170+
if (!parsed || !isGoogleShortHost(parsed.hostname)) {
171+
return { expandedUrl: url, responseText: null };
172+
}
173+
174+
const fetchImpl =
175+
options.fetchImpl ??
176+
(typeof fetch === "function" ? (fetch as unknown as FetchLike) : undefined);
177+
if (!fetchImpl) {
178+
return { expandedUrl: url, responseText: null };
179+
}
180+
181+
const timeoutMs =
182+
typeof options.timeoutMs === "number" && Number.isFinite(options.timeoutMs)
183+
? Math.max(1, Math.floor(options.timeoutMs))
184+
: DEFAULT_EXPAND_TIMEOUT_MS;
185+
186+
try {
187+
const response = await withTimeout(
188+
fetchImpl(parsed.toString(), {
189+
method: "GET",
190+
redirect: "follow",
191+
}),
192+
timeoutMs,
193+
);
194+
195+
let responseText: string | null = null;
196+
if (readResponseText && typeof response.text === "function") {
197+
try {
198+
responseText = await withTimeout(Promise.resolve(response.text()), timeoutMs);
199+
} catch {
200+
responseText = null;
201+
}
202+
}
203+
204+
if (typeof response.url === "string" && response.url.trim()) {
205+
return {
206+
expandedUrl: response.url,
207+
responseText,
208+
};
209+
}
210+
211+
return {
212+
expandedUrl: url,
213+
responseText,
214+
};
215+
} catch {
216+
// Fall back to the original short URL on timeout/fetch failure.
217+
return { expandedUrl: url, responseText: null };
218+
}
219+
}
220+
140221
export function extractFirstUrl(input: string): string | null {
141222
const text = input.trim();
142223
if (!text) return null;
@@ -189,35 +270,8 @@ export async function expandShortMapUrl(
189270
url: string,
190271
options: ExpandShortMapUrlOptions = {},
191272
): Promise<string> {
192-
const parsed = parseUrl(url);
193-
if (!parsed || !isGoogleShortHost(parsed.hostname)) return url;
194-
195-
const fetchImpl =
196-
options.fetchImpl ??
197-
(typeof fetch === "function" ? (fetch as unknown as FetchLike) : undefined);
198-
if (!fetchImpl) return url;
199-
200-
const timeoutMs =
201-
typeof options.timeoutMs === "number" && Number.isFinite(options.timeoutMs)
202-
? Math.max(1, Math.floor(options.timeoutMs))
203-
: DEFAULT_EXPAND_TIMEOUT_MS;
204-
205-
try {
206-
const response = await withTimeout(
207-
fetchImpl(parsed.toString(), {
208-
method: "GET",
209-
redirect: "follow",
210-
}),
211-
timeoutMs,
212-
);
213-
if (typeof response.url === "string" && response.url.trim()) {
214-
return response.url;
215-
}
216-
} catch {
217-
// Fall back to the original short URL on timeout/fetch failure.
218-
}
219-
220-
return url;
273+
const result = await expandShortMapUrlWithResponse(url, options, false);
274+
return result.expandedUrl;
221275
}
222276

223277
export function parseSharedMapLink(input: string): ParsedSharedMapLink | null {
@@ -244,11 +298,22 @@ export async function parseSharedMapLinkAsync(
244298

245299
if (!isGoogleShortHost(url.hostname)) return null;
246300

247-
const expanded = await expandShortMapUrl(extracted, options);
248-
if (expanded === extracted) return null;
301+
const { expandedUrl, responseText } = await expandShortMapUrlWithResponse(extracted, options);
302+
if (expandedUrl !== extracted) {
303+
const expandedParsedUrl = parseUrl(expandedUrl);
304+
if (!expandedParsedUrl) return null;
249305

250-
const expandedUrl = parseUrl(expanded);
251-
if (!expandedUrl) return null;
306+
const parsedExpandedLink = parseSharedMapLinkFromUrl(expandedParsedUrl, extracted);
307+
if (parsedExpandedLink) return parsedExpandedLink;
308+
}
309+
310+
const parsedFromPreviewPayload = parseGooglePreviewCoordinatesFromText(responseText ?? "");
311+
if (!parsedFromPreviewPayload) return null;
252312

253-
return parseSharedMapLinkFromUrl(expandedUrl, extracted);
313+
return {
314+
provider: "google",
315+
lat: parsedFromPreviewPayload.lat,
316+
lon: parsedFromPreviewPayload.lon,
317+
rawUrl: extracted,
318+
};
254319
}

apps/mobile/tests/shared-map-link.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,25 @@ describe("shared map link parser", () => {
166166
});
167167
});
168168

169+
it("parses short links from google preview payload coordinates when url has no coord params", async () => {
170+
const fetchImpl = vi.fn(async () => ({
171+
url: "https://www.google.com/maps/place/Somewhere",
172+
text: async () =>
173+
'<a href="/maps/preview/place?pb=%211m3%212d-1.55405635%213d52.276200949999996">Preview</a>',
174+
}));
175+
176+
const parsed = await parseSharedMapLinkAsync("https://maps.app.goo.gl/RhH3TWEtdqJqRWpw6", {
177+
fetchImpl,
178+
});
179+
180+
expect(parsed).toEqual({
181+
provider: "google",
182+
lat: 52.276200949999996,
183+
lon: -1.55405635,
184+
rawUrl: "https://maps.app.goo.gl/RhH3TWEtdqJqRWpw6",
185+
});
186+
});
187+
169188
it("returns null when expansion cannot produce parseable map coordinates", async () => {
170189
const fetchImpl = vi.fn(async () => ({
171190
url: "https://www.google.com/maps/place/Somewhere",

0 commit comments

Comments
 (0)