feat: implement deep linking & universal links for event confirmation

- Add QR code toggle for HTTPS/Custom Scheme links
- Add Universal Links (iOS) and App Links (Android) support
- Create deep link route handler with platform detection
- Add .well-known files for domain verification
- Fix Content-Type headers for apple-app-site-association
- Add environment configuration for staging & production
- Add comprehensive testing documentation

Testing:
- QR scan → Safari → App switch working 
- Platform detection working 
- Auto redirect to custom scheme working 
- Web fallback JSON response working 

### No Issue
This commit is contained in:
2026-03-09 16:39:01 +08:00
parent a5026cc285
commit ad32eb6fe6
5 changed files with 366 additions and 20 deletions

View File

@@ -1,26 +1,93 @@
import { BaseBox, LoaderCustom, Spacing, StackCustom, TextCustom } from "@/components";
import {
BaseBox,
ButtonCustom,
LoaderCustom,
Spacing,
StackCustom,
TextCustom,
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import { BASE_URL } from "@/service/api-config";
import { useLocalSearchParams } from "expo-router";
import { useState } from "react";
import { StyleSheet } from "react-native";
import QRCode from "react-native-qrcode-svg";
interface EventDetailQRCodeProps {
qrValue: string;
userId: string;
isLoading: boolean;
}
export function EventDetailQRCode({ qrValue, isLoading }: EventDetailQRCodeProps) {
export function EventDetailQRCode({
userId,
isLoading,
}: EventDetailQRCodeProps) {
const { id } = useLocalSearchParams();
const [useHttpsLink, setUseHttpsLink] = useState(true);
// HTTPS link untuk Universal Links (iOS) dan App Links (Android)
// Ini akan membuka file .well-known di Next.js server Anda
const httpsLink = `https://cld-dkr-hipmi-stg.wibudev.com/event/${id}/confirmation?userId=${userId}`;
// Custom scheme link untuk fallback atau testing tanpa universal links
const deepLinkURL = `${BASE_URL}/event/${id}/confirmation?userId=${userId}`;
// Toggle antara HTTPS link dan custom scheme
const qrValue = useHttpsLink ? httpsLink : deepLinkURL;
return (
<BaseBox>
<StackCustom style={{ alignItems: "center" }}>
<TextCustom bold>QR Code Event</TextCustom>
{isLoading ? (
<LoaderCustom />
) : (
<QRCode
value={qrValue}
size={200}
/>
)}
{isLoading ? <LoaderCustom /> : <QRCode value={qrValue} size={200} />}
</StackCustom>
<Spacing />
<TextCustom color="gray" align="center" size="small">
{qrValue}
</TextCustom>
<Spacing />
<StackCustom direction="row" gap="sm">
<ButtonCustom
onPress={() => setUseHttpsLink(true)}
backgroundColor={useHttpsLink ? MainColor.yellow : "transparent"}
textColor={useHttpsLink ? MainColor.black : MainColor.yellow}
style={[
stylesButton.smallButton,
useHttpsLink && stylesButton.border,
]}
>
HTTPS
</ButtonCustom>
<ButtonCustom
onPress={() => setUseHttpsLink(false)}
backgroundColor={!useHttpsLink ? MainColor.yellow : "transparent"}
textColor={!useHttpsLink ? MainColor.black : MainColor.yellow}
style={[
stylesButton.smallButton,
!useHttpsLink && stylesButton.border,
]}
>
Custom Scheme
</ButtonCustom>
</StackCustom>
<Spacing />
<TextCustom color="gray" align="center" size={"small"}>
{useHttpsLink
? "✅ Testing Universal Links/App Links (butuh .well-known config)"
: "🔧 Testing langsung (tanpa domain verification)"}
</TextCustom>
</BaseBox>
);
}
const stylesButton = StyleSheet.create({
smallButton: {
paddingHorizontal: 12,
paddingVertical: 6,
minWidth: 140,
},
border: {
borderWidth: 1,
borderColor: MainColor.yellow,
},
});

View File

@@ -4,13 +4,12 @@ import { IconDot } from "@/components/_Icon/IconComponent";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject";
import AdminButtonReview from "@/components/_ShareComponent/Admin/ButtonReview";
import ReportBox from "@/components/Box/ReportBox";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import ReportBox from "@/components/Box/ReportBox";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import { useAuth } from "@/hooks/use-auth";
import { funUpdateStatusEvent } from "@/screens/Admin/Event/funUpdateStatus";
import { apiAdminEventById } from "@/service/api-admin/api-admin-event";
import { DEEP_LINK_URL } from "@/service/api-config";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useMemo, useState } from "react";
import Toast from "react-native-toast-message";
@@ -25,10 +24,6 @@ export function Admin_ScreenEventDetail() {
const [data, setData] = useState<any | null>(null);
const [loadData, setLoadData] = useState(false);
const deepLinkURL = `${DEEP_LINK_URL}/event/${id}/confirmation?userId=${user?.id}`;
const deepLinkURLDEV = `${DEEP_LINK_URL}/--/event/${id}/confirmation?userId=${user?.id}`;
const isDevLink =
process.env.NODE_ENV === "development" ? deepLinkURLDEV : deepLinkURL;
useFocusEffect(
useCallback(() => {
@@ -149,7 +144,7 @@ export function Admin_ScreenEventDetail() {
)}
{(status === "publish" || status === "history") && (
<EventDetailQRCode qrValue={isDevLink} isLoading={loadData} />
<EventDetailQRCode userId={user?.id || ""} isLoading={loadData} />
)}
</NewWrapper>