Make all external URLs go to the /deep-link/ endpoint to redirect to mobile vs web #139
Merged
trentlarson
merged 7 commits from deep-link-redirect
into master
2 days ago
29 changed files with 509 additions and 154 deletions
@ -0,0 +1,227 @@ |
|||||
|
<template> |
||||
|
<!-- CONTENT --> |
||||
|
<section id="Content" class="relative w-[100vw] h-[100vh]"> |
||||
|
<div |
||||
|
class="p-6 bg-white w-full max-w-[calc((100vh-env(safe-area-inset-top)-env(safe-area-inset-bottom))*0.4)] mx-auto" |
||||
|
> |
||||
|
<div class="mb-4"> |
||||
|
<h1 class="text-xl text-center font-semibold relative mb-4"> |
||||
|
Redirecting to Time Safari |
||||
|
</h1> |
||||
|
|
||||
|
<div v-if="destinationUrl" class="space-y-4"> |
||||
|
<!-- Platform-specific messaging --> |
||||
|
<div class="text-center text-gray-600 mb-4"> |
||||
|
<p v-if="isMobile"> |
||||
|
{{ |
||||
|
isIOS |
||||
|
? "Opening Time Safari app on your iPhone..." |
||||
|
: "Opening Time Safari app on your Android device..." |
||||
|
}} |
||||
|
</p> |
||||
|
<p v-else>Opening Time Safari app...</p> |
||||
|
<p class="text-sm mt-2"> |
||||
|
<span v-if="isMobile" |
||||
|
>If the app doesn't open automatically, use one of these |
||||
|
options:</span |
||||
|
> |
||||
|
<span v-else>Choose how you'd like to open this link:</span> |
||||
|
</p> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Deep Link Button --> |
||||
|
<div class="text-center"> |
||||
|
<a |
||||
|
:href="deepLinkUrl || '#'" |
||||
|
class="inline-block bg-blue-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors" |
||||
|
@click="handleDeepLinkClick" |
||||
|
> |
||||
|
<span v-if="isMobile">Open in Time Safari App</span> |
||||
|
<span v-else>Try Opening in Time Safari App</span> |
||||
|
</a> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Web Fallback Link --> |
||||
|
<div class="text-center"> |
||||
|
<a |
||||
|
:href="webUrl || '#'" |
||||
|
target="_blank" |
||||
|
class="inline-block bg-gray-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-gray-700 transition-colors" |
||||
|
@click="handleWebFallbackClick" |
||||
|
> |
||||
|
<span v-if="isMobile">Open in Web Browser Instead</span> |
||||
|
<span v-else>Open in Web Browser</span> |
||||
|
</a> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Manual Instructions --> |
||||
|
<div class="text-center text-sm text-gray-500 mt-4"> |
||||
|
<p v-if="isMobile"> |
||||
|
Or manually open: |
||||
|
<code class="bg-gray-100 px-2 py-1 rounded">{{ |
||||
|
deepLinkUrl |
||||
|
}}</code> |
||||
|
</p> |
||||
|
<p v-else> |
||||
|
If you have the Time Safari app installed, you can also copy this |
||||
|
link: |
||||
|
<code class="bg-gray-100 px-2 py-1 rounded">{{ |
||||
|
deepLinkUrl |
||||
|
}}</code> |
||||
|
</p> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Platform info for debugging --> |
||||
|
<div |
||||
|
v-if="isDevelopment" |
||||
|
class="text-center text-xs text-gray-400 mt-4" |
||||
|
> |
||||
|
<p> |
||||
|
Platform: {{ isMobile ? (isIOS ? "iOS" : "Android") : "Desktop" }} |
||||
|
</p> |
||||
|
<p>User Agent: {{ userAgent.substring(0, 50) }}...</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div v-else-if="pageError" class="text-center text-red-500 mb-4"> |
||||
|
{{ pageError }} |
||||
|
</div> |
||||
|
|
||||
|
<div v-else class="text-center text-gray-600"> |
||||
|
<p>Processing redirect...</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</section> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Vue } from "vue-facing-decorator"; |
||||
|
import { RouteLocationNormalizedLoaded, Router } from "vue-router"; |
||||
|
|
||||
|
import { APP_SERVER } from "@/constants/app"; |
||||
|
import { logger } from "@/utils/logger"; |
||||
|
import { errorStringForLog } from "@/libs/endorserServer"; |
||||
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; |
||||
|
|
||||
|
@Component({}) |
||||
|
export default class DeepLinkRedirectView extends Vue { |
||||
|
$router!: Router; |
||||
|
$route!: RouteLocationNormalizedLoaded; |
||||
|
pageError: string | null = null; |
||||
|
destinationUrl: string | null = null; // full path after "/deep-link/" |
||||
|
deepLinkUrl: string | null = null; // mobile link starting "timesafari://" |
||||
|
webUrl: string | null = null; // web link, eg "https://timesafari.app/..." |
||||
|
isDevelopment: boolean = false; |
||||
|
userAgent: string = ""; |
||||
|
private platformService = PlatformServiceFactory.getInstance(); |
||||
|
|
||||
|
mounted() { |
||||
|
// Get the path from the route parameter (catch-all parameter) |
||||
|
const pathParam = this.$route.params.path; |
||||
|
|
||||
|
// If pathParam is an array (catch-all parameter), join it |
||||
|
const fullPath = Array.isArray(pathParam) ? pathParam.join("/") : pathParam; |
||||
|
|
||||
|
// Get query parameters from the route |
||||
|
const queryParams = this.$route.query; |
||||
|
|
||||
|
// Build query string if there are query parameters |
||||
|
let queryString = ""; |
||||
|
if (Object.keys(queryParams).length > 0) { |
||||
|
const searchParams = new URLSearchParams(); |
||||
|
Object.entries(queryParams).forEach(([key, value]) => { |
||||
|
if (value !== undefined && value !== null) { |
||||
|
const stringValue = Array.isArray(value) ? value[0] : value; |
||||
|
if (stringValue !== null && stringValue !== undefined) { |
||||
|
searchParams.append(key, stringValue); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
queryString = "?" + searchParams.toString(); |
||||
|
} |
||||
|
|
||||
|
// Combine path with query parameters |
||||
|
const fullPathWithQuery = fullPath + queryString; |
||||
|
|
||||
|
this.destinationUrl = fullPathWithQuery; |
||||
|
this.deepLinkUrl = `timesafari://${fullPathWithQuery}`; |
||||
|
this.webUrl = `${APP_SERVER}/${fullPathWithQuery}`; |
||||
|
|
||||
|
this.isDevelopment = process.env.NODE_ENV !== "production"; |
||||
|
this.userAgent = navigator.userAgent; |
||||
|
|
||||
|
this.openDeepLink(); |
||||
|
} |
||||
|
|
||||
|
private openDeepLink() { |
||||
|
if (!this.deepLinkUrl || !this.webUrl) { |
||||
|
this.pageError = |
||||
|
"No deep link was provided. Check the URL and try again."; |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
// For mobile, try the deep link URL; for desktop, use the web URL |
||||
|
const redirectUrl = this.isMobile ? this.deepLinkUrl : this.webUrl; |
||||
|
|
||||
|
// Method 1: Try window.location.href (works on most browsers) |
||||
|
window.location.href = redirectUrl; |
||||
|
|
||||
|
// Method 2: Fallback - create and click a link element |
||||
|
setTimeout(() => { |
||||
|
try { |
||||
|
const link = document.createElement("a"); |
||||
|
link.href = redirectUrl; |
||||
|
link.style.display = "none"; |
||||
|
document.body.appendChild(link); |
||||
|
link.click(); |
||||
|
document.body.removeChild(link); |
||||
|
} catch (error) { |
||||
|
logger.error( |
||||
|
"Fallback deep link failed: " + errorStringForLog(error), |
||||
|
); |
||||
|
this.pageError = |
||||
|
"Redirecting to the Time Safari app failed. Please use a manual option below."; |
||||
|
} |
||||
|
}, 100); |
||||
|
} catch (error) { |
||||
|
logger.error("Deep link redirect failed: " + errorStringForLog(error)); |
||||
|
this.pageError = |
||||
|
"Unable to open the Time Safari app. Please use a manual option below."; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private handleDeepLinkClick(event: Event) { |
||||
|
if (!this.deepLinkUrl) return; |
||||
|
|
||||
|
// Prevent default to handle the click manually |
||||
|
event.preventDefault(); |
||||
|
|
||||
|
this.openDeepLink(); |
||||
|
} |
||||
|
|
||||
|
private handleWebFallbackClick(event: Event) { |
||||
|
if (!this.webUrl) return; |
||||
|
|
||||
|
// Get platform capabilities |
||||
|
const capabilities = this.platformService.getCapabilities(); |
||||
|
|
||||
|
// For mobile, try to open in a new tab/window |
||||
|
if (capabilities.isMobile) { |
||||
|
event.preventDefault(); |
||||
|
window.open(this.webUrl, "_blank"); |
||||
|
} |
||||
|
// For desktop, let the default behavior happen (opens in same tab) |
||||
|
} |
||||
|
|
||||
|
// Computed properties for template |
||||
|
get isMobile(): boolean { |
||||
|
return this.platformService.getCapabilities().isMobile; |
||||
|
} |
||||
|
|
||||
|
get isIOS(): boolean { |
||||
|
return this.platformService.getCapabilities().isIOS; |
||||
|
} |
||||
|
} |
||||
|
</script> |
Loading…
Reference in new issue