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
1 day 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