forked from trent_larson/crowd-funder-for-time-pwa
add first cut at deep-link redirecting, with one example contact-import that works on mobile
This commit is contained in:
@@ -100,6 +100,7 @@ try {
|
|||||||
- `src/interfaces/deepLinks.ts`: Type definitions and validation schemas
|
- `src/interfaces/deepLinks.ts`: Type definitions and validation schemas
|
||||||
- `src/services/deepLinks.ts`: Deep link processing service
|
- `src/services/deepLinks.ts`: Deep link processing service
|
||||||
- `src/main.capacitor.ts`: Capacitor integration
|
- `src/main.capacitor.ts`: Capacitor integration
|
||||||
|
- `src/views/DeepLinkRedirectView.vue`: Page to handle links to both mobile and web
|
||||||
|
|
||||||
## Type Safety Examples
|
## Type Safety Examples
|
||||||
|
|
||||||
|
|||||||
@@ -219,9 +219,9 @@ export async function logConsoleAndDb(
|
|||||||
isError = false,
|
isError = false,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (isError) {
|
if (isError) {
|
||||||
logger.error(`${new Date().toISOString()} ${message}`);
|
logger.error(`${new Date().toISOString()}`, message);
|
||||||
} else {
|
} else {
|
||||||
logger.log(`${new Date().toISOString()} ${message}`);
|
logger.log(`${new Date().toISOString()}`, message);
|
||||||
}
|
}
|
||||||
await logToDb(message);
|
await logToDb(message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,18 +29,17 @@ import { z } from "zod";
|
|||||||
|
|
||||||
// Add a union type of all valid route paths
|
// Add a union type of all valid route paths
|
||||||
export const VALID_DEEP_LINK_ROUTES = [
|
export const VALID_DEEP_LINK_ROUTES = [
|
||||||
"user-profile",
|
// note that similar lists are below in deepLinkSchemas and in src/services/deepLinks.ts
|
||||||
"project",
|
|
||||||
"onboard-meeting-setup",
|
|
||||||
"invite-one-accept",
|
|
||||||
"contact-import",
|
|
||||||
"confirm-gift",
|
|
||||||
"claim",
|
"claim",
|
||||||
"claim-cert",
|
|
||||||
"claim-add-raw",
|
"claim-add-raw",
|
||||||
"contact-edit",
|
"claim-cert",
|
||||||
"contacts",
|
"confirm-gift",
|
||||||
|
"contact-import",
|
||||||
"did",
|
"did",
|
||||||
|
"invite-one-accept",
|
||||||
|
"onboard-meeting-setup",
|
||||||
|
"project",
|
||||||
|
"user-profile",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
// Create a type from the array
|
// Create a type from the array
|
||||||
@@ -58,44 +57,39 @@ export const routeSchema = z.enum(VALID_DEEP_LINK_ROUTES);
|
|||||||
|
|
||||||
// Parameter validation schemas for each route type
|
// Parameter validation schemas for each route type
|
||||||
export const deepLinkSchemas = {
|
export const deepLinkSchemas = {
|
||||||
"user-profile": z.object({
|
// note that similar lists are above in VALID_DEEP_LINK_ROUTES and in src/services/deepLinks.ts
|
||||||
id: z.string(),
|
|
||||||
}),
|
|
||||||
project: z.object({
|
|
||||||
id: z.string(),
|
|
||||||
}),
|
|
||||||
"onboard-meeting-setup": z.object({
|
|
||||||
id: z.string(),
|
|
||||||
}),
|
|
||||||
"invite-one-accept": z.object({
|
|
||||||
id: z.string(),
|
|
||||||
}),
|
|
||||||
"contact-import": z.object({
|
|
||||||
jwt: z.string(),
|
|
||||||
}),
|
|
||||||
"confirm-gift": z.object({
|
|
||||||
id: z.string(),
|
|
||||||
}),
|
|
||||||
claim: z.object({
|
claim: z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
}),
|
}),
|
||||||
"claim-cert": z.object({
|
|
||||||
id: z.string(),
|
|
||||||
}),
|
|
||||||
"claim-add-raw": z.object({
|
"claim-add-raw": z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
claim: z.string().optional(),
|
claim: z.string().optional(),
|
||||||
claimJwtId: z.string().optional(),
|
claimJwtId: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
"contact-edit": z.object({
|
"claim-cert": z.object({
|
||||||
did: z.string(),
|
id: z.string(),
|
||||||
}),
|
}),
|
||||||
contacts: z.object({
|
"confirm-gift": z.object({
|
||||||
contacts: z.string(), // JSON string of contacts array
|
id: z.string(),
|
||||||
|
}),
|
||||||
|
"contact-import": z.object({
|
||||||
|
jwt: z.string(),
|
||||||
}),
|
}),
|
||||||
did: z.object({
|
did: z.object({
|
||||||
did: z.string(),
|
did: z.string(),
|
||||||
}),
|
}),
|
||||||
|
"invite-one-accept": z.object({
|
||||||
|
jwt: z.string(),
|
||||||
|
}),
|
||||||
|
"onboard-meeting-setup": z.object({
|
||||||
|
id: z.string(),
|
||||||
|
}),
|
||||||
|
project: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
}),
|
||||||
|
"user-profile": z.object({
|
||||||
|
id: z.string(),
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DeepLinkParams = {
|
export type DeepLinkParams = {
|
||||||
|
|||||||
@@ -34,8 +34,7 @@ import router from "./router";
|
|||||||
import { handleApiError } from "./services/api";
|
import { handleApiError } from "./services/api";
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import { DeepLinkHandler } from "./services/deepLinks";
|
import { DeepLinkHandler } from "./services/deepLinks";
|
||||||
import { logConsoleAndDb } from "./db/databaseUtil";
|
import { logger, safeStringify } from "./utils/logger";
|
||||||
import { logger } from "./utils/logger";
|
|
||||||
|
|
||||||
logger.log("[Capacitor] Starting initialization");
|
logger.log("[Capacitor] Starting initialization");
|
||||||
logger.log("[Capacitor] Platform:", process.env.VITE_PLATFORM);
|
logger.log("[Capacitor] Platform:", process.env.VITE_PLATFORM);
|
||||||
@@ -72,10 +71,10 @@ const handleDeepLink = async (data: { url: string }) => {
|
|||||||
await router.isReady();
|
await router.isReady();
|
||||||
await deepLinkHandler.handleDeepLink(data.url);
|
await deepLinkHandler.handleDeepLink(data.url);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleAndDb("[DeepLink] Error handling deep link: " + error, true);
|
logger.error("[DeepLink] Error handling deep link: ", error);
|
||||||
handleApiError(
|
handleApiError(
|
||||||
{
|
{
|
||||||
message: error instanceof Error ? error.message : String(error),
|
message: error instanceof Error ? error.message : safeStringify(error),
|
||||||
} as AxiosError,
|
} as AxiosError,
|
||||||
"deep-link",
|
"deep-link",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -83,6 +83,11 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: "discover",
|
name: "discover",
|
||||||
component: () => import("../views/DiscoverView.vue"),
|
component: () => import("../views/DiscoverView.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/deep-link/:path*",
|
||||||
|
name: "deep-link",
|
||||||
|
component: () => import("../views/DeepLinkRedirectView.vue"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/gifted-details",
|
path: "/gifted-details",
|
||||||
name: "gifted-details",
|
name: "gifted-details",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import { logger } from "../utils/logger";
|
import { logger, safeStringify } from "../utils/logger";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles API errors with platform-specific logging and error processing.
|
* Handles API errors with platform-specific logging and error processing.
|
||||||
@@ -37,7 +37,8 @@ import { logger } from "../utils/logger";
|
|||||||
*/
|
*/
|
||||||
export const handleApiError = (error: AxiosError, endpoint: string) => {
|
export const handleApiError = (error: AxiosError, endpoint: string) => {
|
||||||
if (process.env.VITE_PLATFORM === "capacitor") {
|
if (process.env.VITE_PLATFORM === "capacitor") {
|
||||||
logger.error(`[Capacitor API Error] ${endpoint}:`, {
|
const endpointStr = safeStringify(endpoint); // we've seen this as an object in deep links
|
||||||
|
logger.error(`[Capacitor API Error] ${endpointStr}:`, {
|
||||||
message: error.message,
|
message: error.message,
|
||||||
status: error.response?.status,
|
status: error.response?.status,
|
||||||
data: error.response?.data,
|
data: error.response?.data,
|
||||||
|
|||||||
@@ -27,18 +27,16 @@
|
|||||||
* timesafari://<route>[/<param>][?queryParam1=value1&queryParam2=value2]
|
* timesafari://<route>[/<param>][?queryParam1=value1&queryParam2=value2]
|
||||||
*
|
*
|
||||||
* Supported Routes:
|
* Supported Routes:
|
||||||
* - user-profile: View user profile
|
|
||||||
* - project: View project details
|
|
||||||
* - onboard-meeting-setup: Setup onboarding meeting
|
|
||||||
* - invite-one-accept: Accept invitation
|
|
||||||
* - contact-import: Import contacts
|
|
||||||
* - confirm-gift: Confirm gift
|
|
||||||
* - claim: View claim
|
* - claim: View claim
|
||||||
* - claim-cert: View claim certificate
|
|
||||||
* - claim-add-raw: Add raw claim
|
* - claim-add-raw: Add raw claim
|
||||||
* - contact-edit: Edit contact
|
* - claim-cert: View claim certificate
|
||||||
* - contacts: View contacts
|
* - confirm-gift
|
||||||
|
* - contact-import: Import contacts
|
||||||
* - did: View DID
|
* - did: View DID
|
||||||
|
* - invite-one-accept: Accept invitation
|
||||||
|
* - onboard-meeting-members
|
||||||
|
* - project: View project details
|
||||||
|
* - user-profile: View user profile
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const handler = new DeepLinkHandler(router);
|
* const handler = new DeepLinkHandler(router);
|
||||||
@@ -54,6 +52,7 @@ import {
|
|||||||
} from "../interfaces/deepLinks";
|
} from "../interfaces/deepLinks";
|
||||||
import { logConsoleAndDb } from "../db/databaseUtil";
|
import { logConsoleAndDb } from "../db/databaseUtil";
|
||||||
import type { DeepLinkError } from "../interfaces/deepLinks";
|
import type { DeepLinkError } from "../interfaces/deepLinks";
|
||||||
|
import { logger } from "@/utils/logger";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles processing and routing of deep links in the application.
|
* Handles processing and routing of deep links in the application.
|
||||||
@@ -81,14 +80,15 @@ export class DeepLinkHandler {
|
|||||||
string,
|
string,
|
||||||
{ name: string; paramKey?: string }
|
{ name: string; paramKey?: string }
|
||||||
> = {
|
> = {
|
||||||
|
// note that similar lists are in src/interfaces/deepLinks.ts
|
||||||
claim: { name: "claim" },
|
claim: { name: "claim" },
|
||||||
"claim-add-raw": { name: "claim-add-raw" },
|
"claim-add-raw": { name: "claim-add-raw" },
|
||||||
"claim-cert": { name: "claim-cert" },
|
"claim-cert": { name: "claim-cert" },
|
||||||
"confirm-gift": { name: "confirm-gift" },
|
"confirm-gift": { name: "confirm-gift" },
|
||||||
|
"contact-import": { name: "contact-import", paramKey: "jwt" },
|
||||||
did: { name: "did", paramKey: "did" },
|
did: { name: "did", paramKey: "did" },
|
||||||
"invite-one-accept": { name: "invite-one-accept", paramKey: "jwt" },
|
"invite-one-accept": { name: "invite-one-accept", paramKey: "jwt" },
|
||||||
"onboard-meeting-members": { name: "onboard-meeting-members" },
|
"onboard-meeting-members": { name: "onboard-meeting-members" },
|
||||||
"onboard-meeting-setup": { name: "onboard-meeting-setup" },
|
|
||||||
project: { name: "project" },
|
project: { name: "project" },
|
||||||
"user-profile": { name: "user-profile" },
|
"user-profile": { name: "user-profile" },
|
||||||
};
|
};
|
||||||
@@ -99,7 +99,7 @@ export class DeepLinkHandler {
|
|||||||
*
|
*
|
||||||
* @param url - The deep link URL to parse (format: scheme://path[?query])
|
* @param url - The deep link URL to parse (format: scheme://path[?query])
|
||||||
* @throws {DeepLinkError} If URL format is invalid
|
* @throws {DeepLinkError} If URL format is invalid
|
||||||
* @returns Parsed URL components (path, params, query)
|
* @returns Parsed URL components (path: string, params: {KEY: string}, query: {KEY: string})
|
||||||
*/
|
*/
|
||||||
private parseDeepLink(url: string) {
|
private parseDeepLink(url: string) {
|
||||||
const parts = url.split("://");
|
const parts = url.split("://");
|
||||||
@@ -115,7 +115,16 @@ export class DeepLinkHandler {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [path, queryString] = parts[1].split("?");
|
const [path, queryString] = parts[1].split("?");
|
||||||
const [routePath, param] = path.split("/");
|
const [routePath, ...pathParams] = path.split("/");
|
||||||
|
// logger.log(
|
||||||
|
// "[DeepLink] Debug:",
|
||||||
|
// "Route Path:",
|
||||||
|
// routePath,
|
||||||
|
// "Path Params:",
|
||||||
|
// pathParams,
|
||||||
|
// "Query String:",
|
||||||
|
// queryString,
|
||||||
|
// );
|
||||||
|
|
||||||
// Validate route exists before proceeding
|
// Validate route exists before proceeding
|
||||||
if (!this.ROUTE_MAP[routePath]) {
|
if (!this.ROUTE_MAP[routePath]) {
|
||||||
@@ -134,10 +143,10 @@ export class DeepLinkHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const params: Record<string, string> = {};
|
const params: Record<string, string> = {};
|
||||||
if (param) {
|
if (pathParams) {
|
||||||
// Now we know routePath exists in ROUTE_MAP
|
// Now we know routePath exists in ROUTE_MAP
|
||||||
const routeConfig = this.ROUTE_MAP[routePath];
|
const routeConfig = this.ROUTE_MAP[routePath];
|
||||||
params[routeConfig.paramKey ?? "id"] = param;
|
params[routeConfig.paramKey ?? "id"] = pathParams.join("/");
|
||||||
}
|
}
|
||||||
return { path: routePath, params, query };
|
return { path: routePath, params, query };
|
||||||
}
|
}
|
||||||
@@ -243,6 +252,8 @@ export class DeepLinkHandler {
|
|||||||
code: "INVALID_PARAMETERS",
|
code: "INVALID_PARAMETERS",
|
||||||
message: (error as Error).message,
|
message: (error as Error).message,
|
||||||
details: error,
|
details: error,
|
||||||
|
params: params,
|
||||||
|
query: query,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { logToDb } from "../db/databaseUtil";
|
import { logToDb } from "../db/databaseUtil";
|
||||||
|
|
||||||
function safeStringify(obj: unknown) {
|
export function safeStringify(obj: unknown) {
|
||||||
const seen = new WeakSet();
|
const seen = new WeakSet();
|
||||||
|
|
||||||
return JSON.stringify(obj, (_key, value) => {
|
return JSON.stringify(obj, (_key, value) => {
|
||||||
@@ -67,8 +67,9 @@ export const logger = {
|
|||||||
// Errors will always be logged
|
// Errors will always be logged
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(message, ...args);
|
console.error(message, ...args);
|
||||||
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
|
const messageString = safeStringify(message);
|
||||||
logToDb(message + argsString);
|
const argsString = args.length > 0 ? safeStringify(args) : "";
|
||||||
|
logToDb(messageString + argsString);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1397,7 +1397,8 @@ export default class ContactsView extends Vue {
|
|||||||
const contactsJwt = await createEndorserJwtForDid(this.activeDid, {
|
const contactsJwt = await createEndorserJwtForDid(this.activeDid, {
|
||||||
contacts: selectedContacts,
|
contacts: selectedContacts,
|
||||||
});
|
});
|
||||||
const contactsJwtUrl = APP_SERVER + "/contact-import/" + contactsJwt;
|
const contactsJwtUrl =
|
||||||
|
APP_SERVER + "/deep-link/contact-import/" + contactsJwt;
|
||||||
useClipboard()
|
useClipboard()
|
||||||
.copy(contactsJwtUrl)
|
.copy(contactsJwtUrl)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|||||||
@@ -66,9 +66,14 @@ const formattedPath = computed(() => {
|
|||||||
const path = originalPath.value.replace(/^\/+/, "");
|
const path = originalPath.value.replace(/^\/+/, "");
|
||||||
|
|
||||||
// Log for debugging
|
// Log for debugging
|
||||||
logger.log("Original Path:", originalPath.value);
|
logger.log(
|
||||||
logger.log("Route Params:", route.params);
|
"[DeepLinkError] Original Path:",
|
||||||
logger.log("Route Query:", route.query);
|
originalPath.value,
|
||||||
|
"Route Params:",
|
||||||
|
route.params,
|
||||||
|
"Route Query:",
|
||||||
|
route.query,
|
||||||
|
);
|
||||||
|
|
||||||
return path;
|
return path;
|
||||||
});
|
});
|
||||||
|
|||||||
221
src/views/DeepLinkRedirectView.vue
Normal file
221
src/views/DeepLinkRedirectView.vue
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
<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;
|
||||||
|
this.destinationUrl = fullPath;
|
||||||
|
this.deepLinkUrl = `timesafari://${fullPath}`;
|
||||||
|
this.webUrl = `${APP_SERVER}/${fullPath}`;
|
||||||
|
|
||||||
|
// Log for debugging
|
||||||
|
logger.info("Deep link processing:", {
|
||||||
|
fullPath,
|
||||||
|
deepLinkUrl: this.deepLinkUrl,
|
||||||
|
webUrl: this.webUrl,
|
||||||
|
userAgent: this.userAgent,
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Attempting deep link redirect:", {
|
||||||
|
deepLinkUrl: this.deepLinkUrl,
|
||||||
|
webUrl: this.webUrl,
|
||||||
|
isMobile: this.isMobile,
|
||||||
|
userAgent: this.userAgent,
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
logger.info("Fallback link click completed");
|
||||||
|
} 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>
|
||||||
@@ -519,7 +519,6 @@ export default class HomeView extends Vue {
|
|||||||
// Retrieve DIDs with better error handling
|
// Retrieve DIDs with better error handling
|
||||||
try {
|
try {
|
||||||
this.allMyDids = await retrieveAccountDids();
|
this.allMyDids = await retrieveAccountDids();
|
||||||
logConsoleAndDb(`[HomeView] Retrieved ${this.allMyDids.length} DIDs`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleAndDb(`[HomeView] Failed to retrieve DIDs: ${error}`, true);
|
logConsoleAndDb(`[HomeView] Failed to retrieve DIDs: ${error}`, true);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -552,9 +551,6 @@ export default class HomeView extends Vue {
|
|||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
settings = await retrieveSettingsForActiveAccount();
|
settings = await retrieveSettingsForActiveAccount();
|
||||||
}
|
}
|
||||||
logConsoleAndDb(
|
|
||||||
`[HomeView] Retrieved settings for ${settings.activeDid || "no active DID"}`,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleAndDb(
|
logConsoleAndDb(
|
||||||
`[HomeView] Failed to retrieve settings: ${error}`,
|
`[HomeView] Failed to retrieve settings: ${error}`,
|
||||||
@@ -581,9 +577,6 @@ export default class HomeView extends Vue {
|
|||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
this.allContacts = await db.contacts.toArray();
|
this.allContacts = await db.contacts.toArray();
|
||||||
}
|
}
|
||||||
logConsoleAndDb(
|
|
||||||
`[HomeView] Retrieved ${this.allContacts.length} contacts`,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleAndDb(
|
logConsoleAndDb(
|
||||||
`[HomeView] Failed to retrieve contacts: ${error}`,
|
`[HomeView] Failed to retrieve contacts: ${error}`,
|
||||||
@@ -641,9 +634,6 @@ export default class HomeView extends Vue {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.isRegistered = true;
|
this.isRegistered = true;
|
||||||
logConsoleAndDb(
|
|
||||||
`[HomeView] User ${this.activeDid} is now registered`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleAndDb(
|
logConsoleAndDb(
|
||||||
@@ -685,11 +675,6 @@ export default class HomeView extends Vue {
|
|||||||
this.newOffersToUserHitLimit = offersToUser.hitLimit;
|
this.newOffersToUserHitLimit = offersToUser.hitLimit;
|
||||||
this.numNewOffersToUserProjects = offersToProjects.data.length;
|
this.numNewOffersToUserProjects = offersToProjects.data.length;
|
||||||
this.newOffersToUserProjectsHitLimit = offersToProjects.hitLimit;
|
this.newOffersToUserProjectsHitLimit = offersToProjects.hitLimit;
|
||||||
|
|
||||||
logConsoleAndDb(
|
|
||||||
`[HomeView] Retrieved ${this.numNewOffersToUser} user offers and ` +
|
|
||||||
`${this.numNewOffersToUserProjects} project offers`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleAndDb(
|
logConsoleAndDb(
|
||||||
|
|||||||
Reference in New Issue
Block a user