Browse Source
			
			
			
			
				
		Reviewed-on: https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/pulls/139
				 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