Skip to main content

Tích Hợp Deep Link Trên iOS & Android

Bạn sẽ học: Cách tích hợp Li2 SDK (iOS, Swift Package) hoặc gọi thẳng HTTP API cho cả hai luồng immediate (app đã cài, Universal Link / App Link) và deferred (app chưa cài, clipboard / Install Referrer). Mọi lời gọi mạng đều tới POST /api/v1/track/open.Tab iOS dùng Li2 SDK + SwiftUI (có phần UIKit riêng). Tab Android dùng Jetpack Compose và HTTP API trực tiếp.

Preflight — kiểm 4 thứ này trước khi viết một dòng code

Custom domain đã Verified + huy hiệu Deep Links Active (không phải *.li2.link).
File AASA live: curl -s https://your-domain.com/.well-known/apple-app-site-association | jq trả đúng <TeamID>.<BundleID>.
publishable key dạng li2_pk_... (Settings → Analytics → Publishable Key) — không phải server API key.
Associated Domains trong Xcode khớp đúng domain (applinks:your-domain.com).
Bỏ qua bước này là nguồn của ~80% sự cố thường gặp (401, “app không mở”). Nếu vướng, xem Khắc phục sự cố.

SDK lo gì — bạn lo gì

Cách nhanh nhất: dùng Li2 SDK

Khuyến nghị. Li2 cung cấp Li2 SDK chính thức — Swift Package, iOS 15+, hỗ trợ cả SwiftUI và UIKit. SDK gói sẵn: lời gọi /track/open, cổng “một lần mỗi lần cài”, cửa sổ chờ 250ms, Li2PasteButton (đọc clipboard không bật alert “Allow Paste”), và việc bóc li2_cid khỏi URL đích. Bạn chỉ viết phần điều hướngmàn hình đồng ý.

Cài đặt (Swift Package Manager)

Trong Xcode: File ▸ Add Package Dependencies… và nhập URL:
Ghim Up to Next Major Version từ 0.2.0. Thêm library product Li2SDK vào app target.Yêu cầu: iOS 15.0+, Xcode 15+. Li2PasteButton (đọc clipboard không alert) yêu cầu iOS 16; iOS 15 tự động fallback sang beginRawProbeOptIn() (xem phần màn hình đồng ý bên dưới).

1. Cấu hình SDK một lần khi khởi động

Modifier .li2DeepLink(using:) tự nối .onOpenURL, .onContinueUserActivity, và .task { requestFirstLaunchConsentAfterGrace() } — bạn không cần viết lại ba dòng đó.

2. Sở hữu một resolver; điều hướng theo outcome

SDK gọi closure onOutcome cho mọi kết quả. Bạn điều hướng — SDK không bao giờ tự điều hướng.

3. Màn hình đồng ý (copy-paste — đã kiểm chứng trên thiết bị)

Màn hình này là phần bạn tự sở hữu (SDK không cung cấp sẵn UI). Dưới đây là pattern hoàn chỉnh đã được kiểm chứng. Hai chi tiết được đánh dấu không thể bỏ qua — bỏ là sinh bug.
Hai chi tiết không thể bỏ qua:
  • (a) Empty-clipboard affordanceLi2PasteButton tự disable khi clipboard rỗng. Không có nút thay thế (submitPasteControlEmpty()), người dùng clipboard rỗng bị kẹt và outcome empty không bao giờ được gửi.
  • (b) scenePhase re-probe — clipboard có thể thay đổi khi sheet đang mở (người dùng sang Safari, copy link, quay lại). Probe chỉ trong onAppear sẽ hiện trạng thái nút bị cũ.
Li2PasteButton yêu cầu iOS 16. Trên iOS 15, dùng beginRawProbeOptIn() — nó đọc clipboard trực tiếp, nên iOS sẽ hiện alert “Allow Paste” (hành vi hệ thống của iOS 15, không tránh được). Thêm nhánh fallback vào primaryButton:
Ngược lại, trên iOS 16+ đừng dùng beginRawProbeOptIn() — nó cũng bật alert “Allow Paste”, đúng thứ mà Li2PasteButton sinh ra để tránh.

Kiểm chứng nhanh: in response, đọc matchMethod

Sau khi nối xong, chạy thử và in response của /track/open ra log. Đọc matchMethod (universal_link / clipboard / …) hoặc missReason để biết SDK vừa làm gì — đây là công cụ tự chẩn đoán chính. Bảng đầy đủ: matchMethod & missReason, cây quyết định theo triệu chứng: Khắc phục sự cố.

UIKit integration

Modifier .li2DeepLink(using:) chỉ dành cho SwiftUI. Với UIKit, nối thủ công từ scene delegate:
Theo dõi resolver.isConsentPending (là @Published) qua KVO/Combine để biết khi nào hiện màn hình đồng ý, rồi gọi cùng các method submitPasteControlResult / submitPasteControlEmpty / beginRawProbeOptIn / submitOptOut.
Không có zero-grace entry point. Nếu bạn muốn trì hoãn prompt (ví dụ: sau màn hình onboarding), chỉ cần gọi requestFirstLaunchConsentAfterGrace() muộn hơn — các guard nội bộ đảm bảo lời gọi muộn vẫn đúng, và Universal Link đến trong thời gian đó sẽ tự tắt prompt.

Gặp sự cố?

Các triệu chứng thường gặp — 401 mọi lời gọi, Universal Link không mở app, Li2PasteButton luôn xám, sheet hiện lại sau UL — được tra theo triệu chứng tại trang riêng:

Khắc phục sự cố Deep Link

Chẩn đoán theo triệu chứng + cách đọc matchMethod / missReason để tự gỡ lỗi.

Đo lường chuyển đổi (Conversion Tracking)

Sau khi deep link đã được phân giải, bạn có thể gắn các sự kiện kinh doanh thật — đăng ký, mua hàng — về đúng cú click đã dẫn dắt chúng. Từ 0.2.0, SDK gói sẵn trackLead, trackSaleidentify; một outcome .matched tự lưu Li2.lastClickId (TTL 30 ngày) để các phương thức attributed đọc lại — bạn không phải tự truyền click id.

Đo Lường Chuyển Đổi Cho Deep Link

Hướng dẫn đầy đủ: chọn đúng trackDirect/trackAnonymous/trackAttributed, hành trình ẩn danh ➜ định danh, và cách Android tự gọi HTTP /track/lead + /track/sale.

Tích hợp thủ công (nâng cao) — KHÔNG dùng SDK

Đây là đường THAY THẾ, không phải bước tiếp theo. Nếu bạn đã dùng Li2 SDK ở trên, bỏ qua toàn bộ phần này — SDK đã làm hết. Chỉ đọc tiếp nếu bạn không dùng SDK và muốn tự gọi HTTP, hoặc cần tùy biến sâu hơn những gì SDK cung cấp. Phần này tự dựng lại chính xác những gì Li2PasteButton + resolver đã gói sẵn.

1. Khai báo Associated Domains

Thêm domain Li2 của bạn vào file .entitlements:
Domain này phải trùng với domain đã cấu hình trên dashboard và file AASA mà Li2 phục vụ.

2. Định nghĩa model & lời gọi API dùng chung

Cả luồng immediate lẫn deferred đều gọi cùng một endpoint với cùng một cặp model. Định nghĩa một lần rồi tái sử dụng:
Trong entry point SwiftUI:
handleIncomingURL chỉ gửi deepLink — đây là luồng immediate, không kèm clipboardStatus:
Universal Link immediate không mang li2_cid — nên gửi nguyên url.absoluteString là an toàn, server sẽ phân loại đúng là immediate. li2_cid chỉ được Li2 gieo trên đường chưa cài app (clipboard / install referrer); một cú chạm khi app đã cài được iOS chặn ở chính short link gốc, trước khi có bất kỳ redirect nào sinh li2_cid. (Server quyết định immediate vs deferred dựa trên việc deepLink li2_cid hay không — xem bảng định tuyến trong API.)

4. Khôi phục deferred trong lần mở đầu tiên

Khi người dùng chưa cài app, trang trung gian của Li2 đã chép sẵn link đích kèm li2_cid vào clipboard trước khi đẩy sang App Store. Việc của app chỉ là đọc clipboard đó ở lần mở đầu rồi gửi cho /track/open — bạn không tự dựng li2_cid.
Nhắc lại “Ba định danh”. li2_cid là token 16 ký tự định danh cú click gốc — Li2 gieo nó vào link trên clipboard, bạn chỉ đọc. Đừng nhầm với clickId trong response (server trả về). Bảng đầy đủ ba định danh: Tổng Quan → Ba định danh.
Hai điều kiện về thời điểm:
  • Chờ ≈250ms trước để một Universal Link (nếu có) kịp tới — nếu nó tới, hasReceivedUniversalLink được bật và nhánh deferred bị bỏ qua. 250ms chỉ là đệm; cờ hasReceivedUniversalLink mới là chốt chặn thật, không phải con số thời gian.
  • Chạy đúng một lần mỗi lần cài (cờ firstLaunchRan, lưu bền) để không đọc clipboard / ghi sự kiện lặp ở các lần mở sau.
Mỗi nhánh kết quả map thẳng vào TrackOpenRequest:

Lấy clipboardOutcome bằng UIPasteControl (không bật alert hệ thống)

Bốn nhánh ở trên (.read/.empty/.denied/.optout) đến từ màn hình xin đồng ý. Dùng UIPasteControl: chính cú chạm của người dùng là sự đồng ý, nên iOS không hiện alert “Allow Paste”. Đọc thẳng UIPasteboard.general.string thì sẽ bật alert đó — tránh.UIPasteControl không có bản SwiftUI gốc, phải bọc UIKit. Cạm bẫy quyết định: đặt trong fullScreenCover, control sẽ xám/disabled vì nó dò target qua responder chain mà không tới được — phải gán control.target tường minh và override canPaste(_:) / paste(itemProviders:):
Ánh xạ kết quả về clipboardOutcome: người dùng chạm Paste và nhận chuỗi chứa li2_cid.read; chạm Paste nhưng rỗng/không phải link Li2 → .empty; bỏ qua màn hình (nút “Skip”) → .optout; iOS chặn đọc → .denied.
Cần import UniformTypeIdentifiers cho UTType. Control tự quản label/icon (“Paste”) — bạn chỉ chỉnh được màu, bo góc và display mode.

Idempotency & thử lại

  • Phân giải là idempotent: server đọc bản ghi deferred trên Redis (không tiêu thụ/xóa), nên gọi lại trả về đúng link cho tới khi bản ghi hết hạn (TTL). Một lần thử lại do timeout mạng vẫn khớp như cũ.
  • Nhưng mỗi lần gọi thành công đều ghi một sự kiện analytics. Không có idempotency key phía client → để tránh đếm trùng, gọi /track/open đúng một lần mỗi lần cài (cờ firstLaunchRan + hasReceived*), và chỉ retry trong cùng một lần mở khi gặp lỗi mạng/5xx — đừng retry xuyên nhiều lần mở.

Kiểm thử trên production

Không có host sandbox riêng — bạn kiểm thử ngay trên api.li2.ai bằng hạ tầng thật của mình:
1

Chuẩn bị domain & app thật

Custom domain đã Verified + Deep Links Active, file AASA/assetlinks báo “Live & correct” (xem Cấu Hình Domain).
2

Test immediate (app đã cài)

Cài bản dev qua TestFlight (iOS) / internal-test track hoặc cài trực tiếp (Android), rồi chạm một short link trên domain đó. App phải mở thẳng và /track/open trả universal_link / app_link.
3

Test deferred (chưa cài)

Gỡ app → chạm short link → đi qua trang trung gian → cài lại từ store → mở lần đầu. iOS: chạm Paste trên màn hình đồng ý; Android: Install Referrer tự có. Kỳ vọng clipboard / install_referrer.
Android Install Referrer chỉ trả referrer thật khi cài qua Play Store (internal-test track) — không hoạt động với adb install APK cục bộ. Clipboard của iOS thì test được với mọi kiểu cài.
4

Đối chiếu kết quả

Đọc thẳng matchMethod / missReason trong response, hoặc xem Deep Link Analytics (gói Pro) để xác nhận match/miss.

Bước tiếp theo

Đo Lường Chuyển Đổi

Gắn lead/sale về đúng click sau khi deep link đã phân giải.

Khắc phục sự cố

Tra theo triệu chứng + đọc matchMethod / missReason.

API: POST /track/open

Chi tiết đầy đủ về request, response, matchMethod và missReason.

Cấu Hình Domain

Đảm bảo file AASA / assetlinks đã phục vụ đúng trước khi test.