From ad32eb6fe682137790dd413a918677eaae651903 Mon Sep 17 00:00:00 2001 From: bagasbanuna Date: Mon, 9 Mar 2026 16:39:01 +0800 Subject: [PATCH] feat: implement deep linking & universal links for event confirmation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- app.config.js | 17 +- docs/QR_CODE_TESTING.md | 253 ++++++++++++++++++ .../project.pbxproj | 18 ++ screens/Admin/Event/EventDetailQRCode.tsx | 89 +++++- screens/Admin/Event/ScreenEventDetail.tsx | 9 +- 5 files changed, 366 insertions(+), 20 deletions(-) create mode 100644 docs/QR_CODE_TESTING.md diff --git a/app.config.js b/app.config.js index f3ca81a..372599a 100644 --- a/app.config.js +++ b/app.config.js @@ -1,6 +1,17 @@ // app.config.js require("dotenv").config(); +// const isDev = process.env.NODE_ENV === "development"; +// const isStaging = process.env.NEXT_PUBLIC_ENV === "staging"; +// const isProd = process.env.NEXT_PUBLIC_ENV === "production"; + +// Domain berdasarkan environment +// const domain = isDev +// ? "localhost:3000" +// : isStaging +// ? "cld-dkr-hipmi-stg.wibudev.com" +// : "hipmi.muku.id"; // Production domain + export default { name: "HIPMI Badung Connect", slug: "hipmi-mobile", @@ -20,7 +31,9 @@ export default { NSLocationWhenInUseUsageDescription: "Aplikasi membutuhkan akses lokasi untuk menampilkan peta.", }, - associatedDomains: ["applinks:cld-dkr-staging-hipmi.wibudev.com"], + associatedDomains: [ + "applinks:cld-dkr-hipmi-stg.wibudev.com", + ], buildNumber: "3", }, @@ -41,7 +54,7 @@ export default { data: [ { scheme: "https", - host: "cld-dkr-staging-hipmi.wibudev.com", + host: "cld-dkr-hipmi-stg.wibudev.com", pathPrefix: "/", }, ], diff --git a/docs/QR_CODE_TESTING.md b/docs/QR_CODE_TESTING.md new file mode 100644 index 0000000..57b86ad --- /dev/null +++ b/docs/QR_CODE_TESTING.md @@ -0,0 +1,253 @@ +# QR Code Testing Guide - HIPMI Mobile + +## 📋 Overview + +Dokumentasi ini menjelaskan cara testing QR Code untuk Universal Links (iOS) dan App Links (Android) pada fitur Event Confirmation. + +## 🔧 Update Terbaru + +File `screens/Admin/Event/EventDetailQRCode.tsx` telah diupdate dengan fitur: +- **Toggle Button**: Switch antara HTTPS link dan Custom Scheme link +- **HTTPS Link**: Untuk testing Universal Links/App Links dengan domain staging +- **Custom Scheme**: Untuk testing langsung tanpa domain verification + +## 🎯 Cara Testing QR Code + +### Opsi 1: HTTPS Link (Recommended untuk Production) + +**Gunakan tombol "HTTPS"** di component QR Code. + +**Link yang di-generate:** +``` +https://cld-dkr-staging-hipmi.wibudev.com/event/{id}/confirmation?userId={userId} +``` + +**Cara kerja:** +1. User scan QR code dengan kamera +2. Safari/Chrome terbuka dengan URL HTTPS +3. iOS/Android mendeteksi domain terverifikasi +4. App terbuka otomatis dan menuju halaman confirmation + +**Prerequisites:** +- ✅ File `apple-app-site-association` harus accessible di Next.js server +- ✅ File `assetlinks.json` harus accessible di Next.js server +- ✅ Domain harus terverifikasi di app.config.js +- ✅ App harus di-build ulang setelah perubahan domain + +**Testing Steps:** +```bash +# 1. Pastikan .well-known files accessible +curl https://cld-dkr-staging-hipmi.wibudev.com/.well-known/apple-app-site-association +curl https://cld-dkr-staging-hipmi.wibudev.com/.well-known/assetlinks.json + +# 2. Rebuild app +bunx expo prebuild --clean + +# 3. Run di physical device (bukan simulator) +bun run android # untuk Android +bun run ios # untuk iOS +``` + +### Opsi 2: Custom Scheme Link (Untuk Development/Testing Cepat) + +**Gunakan tombol "Custom Scheme"** di component QR Code. + +**Link yang di-generate:** +``` +hipmimobile://event/{id}/confirmation?userId={userId} +``` + +**Cara kerja:** +1. User scan QR code dengan kamera +2. iOS: Pilih "Open in HIPMI Badung Connect" +3. Android: Langsung buka app +4. App terbuka dan menuju halaman confirmation + +**Keuntungan:** +- ✅ Tidak butuh domain verification +- ✅ Bisa testing langsung tanpa rebuild +- ✅ Cocok untuk development + +**Kekurangan:** +- ❌ Tidak bisa dibuka dari web browser +- ❌ Tidak support universal linking dari website lain + +## 📱 Testing Checklist + +### iOS (Universal Links) + +- [ ] File `apple-app-site-association` valid dan accessible +- [ ] Domain terdaftar di `app.config.js` → `ios.associatedDomains` +- [ ] Bundle ID match dengan konfigurasi +- [ ] Team ID benar di apple-app-site-association +- [ ] Test dengan **physical device** (simulator tidak support) +- [ ] Test dengan **Safari** (bukan Chrome) +- [ ] Long press link → ada opsi "Open" + +**Debug iOS:** +```bash +# Cek apple-app-site-association +curl -I https://cld-dkr-staging-hipmi.wibudev.com/.well-known/apple-app-site-association + +# Harus return: +# Content-Type: application/json +# HTTP/2 200 +``` + +### Android (App Links) + +- [ ] File `assetlinks.json` valid dan accessible +- [ ] SHA256 fingerprint benar +- [ ] Package name match +- [ ] Intent filters terdaftar di app.config.js +- [ ] Test dengan **physical device** +- [ ] Test dengan **Chrome** + +**Debug Android:** +```bash +# Dapatkan SHA256 fingerprint +cd android +./gradlew signingReport + +# Cek assetlinks.json +curl https://cld-dkr-staging-hipmi.wibudev.com/.well-known/assetlinks.json +``` + +## 🐛 Troubleshooting + +### Problem: QR Scan Terbuka di Safari, Tidak Balik ke App + +**Penyebab:** +- Domain belum terverifikasi untuk Universal Links/App Links +- File `.well-known` tidak accessible atau invalid +- App belum di-rebuild setelah perubahan domain + +**Solusi:** +1. Pastikan file `.well-known` accessible: + ```bash + curl https://cld-dkr-staging-hipmi.wibudev.com/.well-known/apple-app-site-association + ``` + +2. Rebuild app: + ```bash + bunx expo prebuild --clean + bun run android # atau bun run ios + ``` + +3. Gunakan **Custom Scheme** untuk testing cepat + +### Problem: Link Tidak Membuka App Sama Sekali + +**Cek:** +1. App sudah terinstall di device +2. Link format benar (hipmimobile:// atau https://) +3. Route handler sudah ada di app folder + +**Test manual:** +```bash +# iOS Simulator +xcrun simctl openurl booted "hipmimobile://event/123/confirmation?userId=456" + +# Android Emulator +adb shell am start -W -a android.intent.action.VIEW \ + -d "hipmimobile://event/123/confirmation?userId=456" \ + com.bip.hipmimobileapp +``` + +### Problem: "Cannot GET /event/..." di Next.js + +**Penyebab:** +Route `/event/[id]/confirmation` tidak ada di Next.js server + +**Solusi:** +Pastikan Next.js project punya file: +``` +public/.well-known/apple-app-site-association +public/.well-known/assetlinks.json +``` + +Dan API route untuk handle: +``` +pages/api/event/[id]/confirmation.ts +``` + +## 📄 File Configuration + +### app.config.js - iOS +```javascript +ios: { + associatedDomains: ["applinks:cld-dkr-staging-hipmi.wibudev.com"], +} +``` + +### app.config.js - Android +```javascript +android: { + intentFilters: [ + { + action: "VIEW", + autoVerify: true, + data: [ + { + scheme: "https", + host: "cld-dkr-staging-hipmi.wibudev.com", + pathPrefix: "/", + }, + ], + category: ["BROWSABLE", "DEFAULT"], + }, + ], +} +``` + +### apple-app-site-association (Next.js) +```json +{ + "applinks": { + "apps": [], + "details": [ + { + "appID": "TEAM_ID.com.anonymous.hipmi-mobile", + "paths": ["/event/*/confirmation"] + } + ] + } +} +``` + +### assetlinks.json (Next.js) +```json +[ + { + "relation": ["delegate_permission/common.handle_all_urls"], + "target": { + "namespace": "android_app", + "package_name": "com.bip.hipmimobileapp", + "sha256_cert_fingerprints": ["YOUR_SHA256_FINGERPRINT"] + } + } +] +``` + +## 🎓 Best Practices + +1. **Development**: Gunakan Custom Scheme untuk testing cepat +2. **Staging**: Gunakan HTTPS link dengan domain staging +3. **Production**: Gunakan HTTPS link dengan domain production +4. **Testing**: Selalu test di physical device, bukan simulator +5. **Debugging**: Enable logging di confirmation page untuk track deep link + +## 🔗 Related Files + +- `screens/Admin/Event/EventDetailQRCode.tsx` - QR Code generator +- `app/(application)/(user)/event/[id]/confirmation.tsx` - Confirmation page +- `app.config.js` - App configuration +- `service/api-config.ts` - API configuration (DEEP_LINK_URL) + +## 📞 Support + +Jika masih ada masalah: +1. Cek logs di console +2. Test manual dengan adb/xcrun +3. Verify .well-known files dengan curl +4. Pastikan app rebuild setelah perubahan config diff --git a/ios/HIPMIBadungConnect.xcodeproj/project.pbxproj b/ios/HIPMIBadungConnect.xcodeproj/project.pbxproj index d7d08be..7784b2a 100644 --- a/ios/HIPMIBadungConnect.xcodeproj/project.pbxproj +++ b/ios/HIPMIBadungConnect.xcodeproj/project.pbxproj @@ -183,6 +183,7 @@ FB0CB57BF4D74C1D87C2036C /* Remove signature files (Xcode workaround) */, 14B3DE54EE4049AEB1EADA6B /* Remove signature files (Xcode workaround) */, B4CF5E09DBB44A4FB9CB91B9 /* Remove signature files (Xcode workaround) */, + C894BD25C8224984AAD73398 /* Remove signature files (Xcode workaround) */, ); buildRules = ( ); @@ -995,6 +996,23 @@ rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\"; "; }; + C894BD25C8224984AAD73398 /* Remove signature files (Xcode workaround) */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + name = "Remove signature files (Xcode workaround)"; + inputPaths = ( + ); + outputPaths = ( + ); + shellPath = /bin/sh; + shellScript = " + echo \"Remove signature files (Xcode workaround)\"; + rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\"; + "; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ diff --git a/screens/Admin/Event/EventDetailQRCode.tsx b/screens/Admin/Event/EventDetailQRCode.tsx index 7604aa6..b7b5cef 100644 --- a/screens/Admin/Event/EventDetailQRCode.tsx +++ b/screens/Admin/Event/EventDetailQRCode.tsx @@ -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 ( QR Code Event - {isLoading ? ( - - ) : ( - - )} + {isLoading ? : } + + {qrValue} + + + + setUseHttpsLink(true)} + backgroundColor={useHttpsLink ? MainColor.yellow : "transparent"} + textColor={useHttpsLink ? MainColor.black : MainColor.yellow} + style={[ + stylesButton.smallButton, + useHttpsLink && stylesButton.border, + ]} + > + HTTPS + + setUseHttpsLink(false)} + backgroundColor={!useHttpsLink ? MainColor.yellow : "transparent"} + textColor={!useHttpsLink ? MainColor.black : MainColor.yellow} + style={[ + stylesButton.smallButton, + !useHttpsLink && stylesButton.border, + ]} + > + Custom Scheme + + + + + {useHttpsLink + ? "✅ Testing Universal Links/App Links (butuh .well-known config)" + : "🔧 Testing langsung (tanpa domain verification)"} + ); } + +const stylesButton = StyleSheet.create({ + smallButton: { + paddingHorizontal: 12, + paddingVertical: 6, + minWidth: 140, + }, + border: { + borderWidth: 1, + borderColor: MainColor.yellow, + }, +}); diff --git a/screens/Admin/Event/ScreenEventDetail.tsx b/screens/Admin/Event/ScreenEventDetail.tsx index 195676d..6abb9f5 100644 --- a/screens/Admin/Event/ScreenEventDetail.tsx +++ b/screens/Admin/Event/ScreenEventDetail.tsx @@ -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(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") && ( - + )}