From 8c06f49b02fa4c910ce30bd521b11b0b3b97d07b Mon Sep 17 00:00:00 2001 From: Zdenek Stursa <90236729+zdenek-stursa@users.noreply.github.com> Date: Sun, 10 May 2026 20:21:16 +0200 Subject: [PATCH] fix: make PWA share target functional on Android Chrome (#7468) Co-authored-by: Zdenek Co-authored-by: Claude Sonnet 4.6 --- .../pwa-share-target-redirect.global.ts | 7 ++-- .../app/pages/g/[groupSlug]/r/create/url.vue | 32 +++++++++++++---- frontend/app/pages/login.vue | 36 ++++++++++++++++--- mealie/routes/spa/manifest.py | 8 ++++- 4 files changed, 68 insertions(+), 15 deletions(-) diff --git a/frontend/app/middleware/pwa-share-target-redirect.global.ts b/frontend/app/middleware/pwa-share-target-redirect.global.ts index 071436733..d7b7e99fd 100644 --- a/frontend/app/middleware/pwa-share-target-redirect.global.ts +++ b/frontend/app/middleware/pwa-share-target-redirect.global.ts @@ -3,8 +3,11 @@ export default defineNuxtRouteMiddleware((to) => { const { user } = useMealieAuth(); const groupSlug = user.value?.groupSlug; if (!groupSlug) { - return navigateTo("/login", { redirectCode: 301 }); + // Preserve the full path (including recipe_import_url query param) so the + // login page can redirect back here after successful authentication. + const redirect = encodeURIComponent(to.fullPath); + return navigateTo(`/login?redirect=${redirect}`, { redirectCode: 302 }); } - return navigateTo(`/g/${groupSlug}${to.fullPath}`, { redirectCode: 301 }); + return navigateTo(`/g/${groupSlug}${to.fullPath}`, { redirectCode: 302 }); } }); diff --git a/frontend/app/pages/g/[groupSlug]/r/create/url.vue b/frontend/app/pages/g/[groupSlug]/r/create/url.vue index 10eceb924..f244e6d21 100644 --- a/frontend/app/pages/g/[groupSlug]/r/create/url.vue +++ b/frontend/app/pages/g/[groupSlug]/r/create/url.vue @@ -198,15 +198,32 @@ const recipeUrl = computed({ } }, get() { - return route.query.recipe_import_url as string | null; + // Prefer the 'url' share field (recipe_import_url, populated by Chrome when + // sharing a page URL). Fall back to the 'text' share field (recipe_import_text) + // for apps that share URLs as plain text, but only when the text value is + // actually a valid http/https URL — shared text can be arbitrary. + const urlFromField = route.query.recipe_import_url as string | null; + if (urlFromField) { + return urlFromField; + } + const textFromField = route.query.recipe_import_text as string | null; + if (textFromField) { + try { + const parsed = new URL(textFromField); + if (parsed.protocol === "http:" || parsed.protocol === "https:") { + return textFromField; + } + } + catch { /* not a URL, ignore */ } + } + return null; }, }); onMounted(() => { - if (recipeUrl.value && recipeUrl.value.includes("https")) { - // Check if we have a query params for using keywords as tags or staying in edit mode. - // We don't use these in the app anymore, but older automations such as Bookmarklet might still use them, - // and they're easy enough to support. + if (recipeUrl.value) { + // Apply legacy query params for older automations such as the Bookmarklet. + // These are no longer used by the app itself but are easy to keep supporting. const importKeywordsAsTagsParam = route.query.use_keywords; if (importKeywordsAsTagsParam === "1") { importKeywordsAsTags.value = true; @@ -223,8 +240,9 @@ onMounted(() => { stayInEditMode.value = false; } - createByUrl(recipeUrl.value, importKeywordsAsTags.value, false); - return; + // The URL is pre-filled via the recipeUrl computed property. + // Do not auto-submit: the user should review the import options and + // confirm by clicking the submit button. } }); diff --git a/frontend/app/pages/login.vue b/frontend/app/pages/login.vue index b30bc24c6..7b9cd4acc 100644 --- a/frontend/app/pages/login.vue +++ b/frontend/app/pages/login.vue @@ -211,7 +211,7 @@