Skip to content

Commit a0f8ebc

Browse files
Add Android deep link interstitial (dubinc#3836)
Co-authored-by: Steven Tey <stevensteel97@gmail.com>
1 parent 1d3c527 commit a0f8ebc

3 files changed

Lines changed: 75 additions & 9 deletions

File tree

apps/web/app/app.dub.co/(deeplink)/deeplink/[domain]/[[...key]]/action-buttons.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
"use client";
22

33
import { Link } from "@dub/prisma/client";
4-
import { Button, IOSAppStore, useCopyToClipboard } from "@dub/ui";
4+
import { AndroidLogo, Button, IOSAppStore, useCopyToClipboard } from "@dub/ui";
55
import { useSearchParams } from "next/navigation";
66
import { getTranslations, Language } from "./translations";
77

88
export function DeepLinkActionButtons({
99
link,
1010
language,
11+
platform,
1112
}: {
1213
link: Pick<Link, "shortLink">;
1314
language: Language;
15+
platform: "ios" | "android";
1416
}) {
1517
const t = getTranslations(language);
1618
const searchParams = useSearchParams();
@@ -28,14 +30,16 @@ export function DeepLinkActionButtons({
2830
window.location.href = `${link.shortLink}?skip_deeplink_preview=1${searchParamsString ? `&${searchParamsString}` : ""}`;
2931
};
3032

33+
const Icon = platform === "android" ? AndroidLogo : IOSAppStore;
34+
3135
return (
3236
<div className="flex flex-col items-center gap-4">
3337
<Button
3438
text={t.openInApp}
3539
className="h-12 w-full rounded-xl bg-neutral-900 text-white"
3640
variant="primary"
3741
onClick={() => handleClick({ withCopy: true })}
38-
icon={<IOSAppStore className="size-6" />}
42+
icon={<Icon className="size-6" />}
3943
/>
4044

4145
<button

apps/web/app/app.dub.co/(deeplink)/deeplink/[domain]/[[...key]]/page.tsx

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,18 @@ import {
55
import { deepViewDataSchema } from "@/lib/zod/schemas/deep-links";
66
import { prisma } from "@dub/prisma";
77
import { Grid, Wordmark } from "@dub/ui";
8-
import { ArrowRight, Copy, IOSAppStore, MobilePhone } from "@dub/ui/icons";
8+
import {
9+
AndroidLogo,
10+
ArrowRight,
11+
Copy,
12+
IOSAppStore,
13+
MobilePhone,
14+
} from "@dub/ui/icons";
915
import { cn } from "@dub/utils";
1016
import { headers } from "next/headers";
1117
import Link from "next/link";
1218
import { redirect } from "next/navigation";
19+
import { userAgent } from "next/server";
1320
import { DeepLinkActionButtons } from "./action-buttons";
1421
import { BrandLogoBadge } from "./brand-logo-badge";
1522
import { getLanguage, getTranslations } from "./translations";
@@ -27,6 +34,10 @@ export default async function DeepLinkPreviewPage(props: {
2734
const language = getLanguage(acceptLanguage);
2835
const t = getTranslations(language);
2936

37+
const ua = userAgent({ headers: headersList });
38+
const platform: "ios" | "android" =
39+
ua.os?.name === "Android" ? "android" : "ios";
40+
3041
// Encode the key for case-sensitive domains before querying
3142
const encodedKey = encodeKeyIfCaseSensitive({
3243
domain,
@@ -46,9 +57,11 @@ export default async function DeepLinkPreviewPage(props: {
4657
shortLink: true,
4758
url: true,
4859
ios: true,
60+
android: true,
4961
shortDomain: {
5062
select: {
5163
appleAppSiteAssociation: true,
64+
assetLinks: true,
5265
deepviewData: true,
5366
},
5467
},
@@ -62,10 +75,16 @@ export default async function DeepLinkPreviewPage(props: {
6275

6376
const deepViewData = deepViewDataSchema.parse(link.shortDomain.deepviewData);
6477

65-
// if the link domain doesn't have an AASA file configured (or deepviewData is null)
66-
// we skip the deep link preview and redirect to the link's URL
67-
if (!link.shortDomain.appleAppSiteAssociation || !deepViewData) {
68-
redirect(link.ios ?? link.url);
78+
// if the domain isn't set up for deep linking on the user's platform, skip
79+
// the preview and forward to the platform-specific URL (or the canonical URL)
80+
if (platform === "android") {
81+
if (!link.shortDomain.assetLinks || !deepViewData) {
82+
redirect(link.android ?? link.url);
83+
}
84+
} else {
85+
if (!link.shortDomain.appleAppSiteAssociation || !deepViewData) {
86+
redirect(link.ios ?? link.url);
87+
}
6988
}
7089

7190
// decode the link if the domain is case sensitive
@@ -149,14 +168,22 @@ export default async function DeepLinkPreviewPage(props: {
149168
<div className="flex items-center justify-center gap-3">
150169
<Copy className="text-content-default size-6" />
151170
<ArrowRight className="text-content-subtle size-3" />
152-
<IOSAppStore className="text-content-default size-6" />
171+
{platform === "android" ? (
172+
<AndroidLogo className="text-content-default size-6" />
173+
) : (
174+
<IOSAppStore className="text-content-default size-6" />
175+
)}
153176
<ArrowRight className="text-content-subtle size-3" />
154177
<MobilePhone className="text-content-default size-6" />
155178
</div>
156179
</div>
157180
</div>
158181

159-
<DeepLinkActionButtons link={link} language={language} />
182+
<DeepLinkActionButtons
183+
link={link}
184+
language={language}
185+
platform={platform}
186+
/>
160187
</div>
161188
</div>
162189
</main>

apps/web/lib/middleware/link.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { detectBot } from "./utils/detect-bot";
3030
import { getFinalUrl } from "./utils/get-final-url";
3131
import { getIdentityHash } from "./utils/get-identity-hash";
3232
import { isAppsFlyerTrackingUrl } from "./utils/is-appsflyer-tracking-url";
33+
import { isGooglePlayStoreUrl } from "./utils/is-google-play-store-url";
3334
import { isIosAppStoreUrl } from "./utils/is-ios-app-store-url";
3435
import { isSingularTrackingUrl } from "./utils/is-singular-tracking-url";
3536
import { isSupportedCustomURIScheme } from "./utils/is-supported-custom-uri-scheme";
@@ -498,6 +499,40 @@ export async function LinkMiddleware(req: NextRequest, ev: NextFetchEvent) {
498499
}),
499500
);
500501

502+
// if it's a Google Play Store URL (and skip_deeplink_preview is not set)
503+
// we need to show the interstitial page + cache deep link click data
504+
if (
505+
isGooglePlayStoreUrl(android) &&
506+
!req.nextUrl.searchParams.get("skip_deeplink_preview")
507+
) {
508+
ev.waitUntil(
509+
cacheDeepLinkClickData({
510+
req,
511+
clickId,
512+
link: {
513+
id: linkId,
514+
domain,
515+
key,
516+
url, // pass the main destination URL to the cache (for deferred deep linking)
517+
},
518+
}),
519+
);
520+
521+
// redirect to the deeplink interstitial splash page "DeepLinkPreviewPage"
522+
return createResponseWithCookies(
523+
NextResponse.redirect(
524+
new URL(`/deeplink/${domain}${fullPath}`, APP_DOMAIN),
525+
{
526+
headers: {
527+
...DUB_HEADERS,
528+
...(!shouldIndex && { "X-Robots-Tag": "googlebot: noindex" }),
529+
},
530+
},
531+
),
532+
cookieData,
533+
);
534+
}
535+
501536
return createResponseWithCookies(
502537
NextResponse.redirect(finalUrl, {
503538
headers: {

0 commit comments

Comments
 (0)