จะเชื่อมโยงพารามิเตอร์กำหนดเองเมื่อสร้าง QR code ได้อย่างไร? การเชื่อมโยงพารามิเตอร์กำหนดเองสำหรับการนำทางในแอป จำเป็นต้องผนวกคีย์คิวรี URL ที่มีโครงสร้างเข้ากับลิงก์หน้าแลนดิ้งเพจที่โฮสต์ไว้ จับคีย์เหล่านั้นผ่านเลเยอร์การบูรณาการเว็บ และส่งเพย์โหลดที่ผ่านการตรวจสอบไปยังตัวควบคุมการนำทางเนทีฟ เมื่อแอปปลุกการทำงานโดยตรงหรือเปิดใช้งานหลังการติดตั้ง
การนำทางในแอปผ่าน QR code คือสถาปัตยกรรมการนำทางแบบไคลเอนต์-เซิร์ฟเวอร์ ที่ฝังพารามิเตอร์ปลายทางที่มีโครงสร้างไว้ใน URL ของบาร์โค้ดแบบสองมิติ การแก้พารามิเตอร์เหล่านี้ผ่านลิงก์เชิงลึกของระบบปฏิบัติการโดยตรงเมื่อมีการติดตั้งแอปพลิเคชันแล้ว หรือผ่านการกู้คืนแบบดีเฟอร์รีดที่รองรับโดยแพลตฟอร์มเมื่อมีการติดตั้งแอปพลิเคชันใหม่จากสโตร์ จะช่วยให้ทีมวิศวกรรมสามารถนำทางผู้ใช้ไปยังหน้าจอเฉพาะภายในแอปได้โดยตรง แทนที่จะเป็นหน้าจอหลักเริ่มต้น
| คำศัพท์ | คำนิยาม | เอนทิตีที่เกี่ยวข้อง | บทบาทความตั้งใจในการค้นหา |
|---|---|---|---|
| Create QR Code for App | เวิร์กโฟลว์ทางวิศวกรรมในการเข้ารหัส URL การนำทางที่ใช้งานได้ลงในบาร์โค้ด 2 มิติสำหรับการนำทางบนมือถือ | Scene Restoration | เชิงข้อมูล / เทคนิค |
| app qr code | แอสเซทบาร์โค้ดที่กำหนดค่าเพื่อทริกเกอร์การเปิดใช้งานแอปพลิเคชันเนทีฟหรือการเริ่มต้นใช้งานผ่านสโตร์ | Universal Links / App Links | เชิงเทคนิค / เชิงพาณิชย์ |
| dynamic qr code | บาร์โค้ดที่ชี้ไปยังปลายทางเส้นทางการนำทางตัวกลาง ซึ่งสามารถอัปเดตเพย์โหลดปลายทางได้ทางฝั่งเซิร์ฟเวอร์ | URI Indirection | เชิงพาณิชย์ |
| qr code tracking | การวัดคำขอขาเข้าของการนำทางและการเปิดใช้งานหน้าจอภายในแอปพลิเคชันปลายทาง | Channel Statistics & Tracking | เชิงพาณิชย์ |
ภาพรวมสถาปัตยกรรม: วิธีสร้าง QR Code สำหรับการกู้คืนฉากในแอป (Scene Restoration)

ปัญหาการละทิ้งที่หน้าจอหลัก: เหตุใดการเปลี่ยนเส้นทางไปยังสโตร์ทั่วไปจึงขัดขวางเส้นทางผู้ใช้ที่มีความตั้งใจสูง
QR code ทางกายภาพมักถูกนำไปใช้ในสภาพแวดล้อมบริบทที่มีความตั้งใจสูง: นักช้อปในร้านค้าปลีกสแกนป้ายราคาบนชั้นวางเพื่อดูข้อมูลจำเพาะของสินค้า ผู้รับประทานอาหารสแกนป้ายบนโต๊ะเพื่อสั่งอาหารจากหมวดหมู่เมนูเฉพาะ หรือผู้เล่นสแกนบาร์โค้ดคำเชิญเพื่อเข้าร่วมล็อบบี้เกมหลายผู้เล่น เมื่อบาร์โค้ดเข้ารหัสลิงก์ App Store ทั่วไปหรือโดเมนที่ไม่ได้กำหนดพารามิเตอร์ ความตั้งใจตามบริบทของผู้ใช้จะสูญหายไปในระหว่างการเปลี่ยนผ่านจากการสแกนทางกายภาพไปยังรันไทม์ของแอปพลิเคชันเนทีฟ
เมื่อเปิดแอปพลิเคชัน ผู้ใช้จะถูกพามาที่หน้าจอหลักระดับบนสุดเริ่มต้น การบังคับให้ผู้ใช้ต้องนำทางผ่านโครงสร้างหมวดหมู่ที่ซับซ้อนด้วยตนเอง กรอกคำค้นหาซ้ำ หรือพิมพ์ตัวระบุห้อง จะสร้างอุปสรรคทางความคิดและกลไกที่สำคัญ การกู้คืนฉากในแอปจะช่วยแก้ปัญหาการละทิ้งนี้โดยการเข้ารหัสสถานะปลายทางลงใน URL ของ QR code และสร้างมุมมองเป้าหมายนั้นขึ้นใหม่แบบโปรแกรมมิงภายในไคลเอนต์เนทีฟ
กลไกการนำทางแบบสองสถานะ: การปลุกการทำงานโดยตรงเทียบกับการกู้คืนฉากแบบดีเฟอร์รีด

เมื่อผู้ใช้สแกน QR code สำหรับการนำทางในแอป อุปกรณ์มือถือจะอยู่ในสถานะการติดตั้งหนึ่งในสองสถานะ สถาปัตยกรรมการนำทางที่สมบูรณ์จะจัดการวงจรชีวิตทั้งสองรูปแบบผ่านเส้นทางทางเทคนิคที่แตกต่างกัน:
- การนำทางเพื่อปลุกการทำงานโดยตรง (ติดตั้งแอปพลิเคชันแล้ว): เมื่อมีแอปพลิเคชันเป้าหมายอยู่แล้วบนอุปกรณ์ การสแกน QR code จะทริกเกอร์ลิงก์เชิงลึกของระบบปฏิบัติการ—โดยเฉพาะ Apple Universal Links บน iOS หรือ Android App Links บน Android กลไกทั้งสองต้องอาศัยการโฮสต์ไฟล์เชื่อมโยงโดเมนที่ได้รับการตรวจสอบความถูกต้องด้วยการเข้ารหัส: iOS ต้องการไฟล์
apple-app-site-association(AASA) ที่จับคู่กับสิทธิ์Associated Domainsใน Xcode ในขณะที่ Android ต้องการไฟล์ Digital Asset Links/.well-known/assetlinks.jsonที่ตรงกับลายนิ้วมือ SHA-256 ของใบรับรองการเซ็นชื่อแอปพลิเคชันและตัวกรอง Intent ของแมนิเฟสต์ที่ได้รับการตรวจสอบแล้ว เมื่อได้รับการตรวจสอบแล้ว ระบบปฏิบัติการจะปลุกแอปพลิเคชันและส่ง URI ไปยังตัวแทนวงจรชีวิตของไคลเอนต์โดยตรง - การกู้คืนฉากแบบดีเฟอร์รีด (ยังไม่ได้ติดตั้งแอปพลิเคชัน): เมื่อไม่มีแอปพลิเคชัน ผู้ใช้จะถูกนำทางผ่านหน้าแลนดิ้งเพจเว็บตัวกลางไปยัง App Store ของแพลตฟอร์ม เนื่องจากยังไม่ได้ติดตั้งไบนารีเนทีฟเพื่อรับ Intent ของระบบปฏิบัติการ พารามิเตอร์การนำทางจึงต้องถูกเตรียมไว้ก่อนการเปลี่ยนเส้นทางไปยังสโตร์ และกู้คืนระหว่างการเปิดใช้งานครั้งแรกของแอปพลิเคชัน (การเริ่มต้นใช้งานแบบเย็นหรือ Day 0 cold start) หลังจากติดตั้งเสร็จสิ้น
ขอบเขตการกู้คืนเฉพาะแพลตฟอร์ม: Google Play Install Referrer, iOS Universal Links และ SDK Adapters
หลังจากผู้ใช้ติดตั้งแอปพลิเคชันจาก App Store การกู้คืนพารามิเตอร์การอ้างอิงและการนำทางจะขึ้นอยู่กับแพลตฟอร์มและกลไกที่ใช้งานโดยแอปพลิเคชันหรือ Attribution SDK การส่งพารามิเตอร์ route หรือ id ผ่าน URL เว็บ ไม่ได้เป็นการรับประกันในตัวมันเองว่าพารามิเตอร์นั้นจะยังคงอยู่หลังจากการติดตั้งผ่าน App Store:
- Android และ Google Play: Google Play Install Referrer API สามารถให้เมทาเดตาการอ้างอิงที่รองรับซึ่งเกี่ยวข้องกับการติดตั้งผ่าน Play Store กลไกนี้ใช้เฉพาะกับการติดตั้งที่เสร็จสมบูรณ์ผ่านเส้นทาง Google Play Store อย่างเป็นทางการเท่านั้น ไม่ใช่ไฟล์ APK ไซด์โหลดจากบุคคลที่สามหรือสโตร์ทางเลือกอื่น ๆ แอปพลิเคชันต้องกำหนดค่าและทดสอบเพย์โหลดรีเฟอร์เรอร์จริง และตรวจสอบว่าพารามิเตอร์ที่คาดหวังยังคงอยู่หลังจากการเปลี่ยนเส้นทางและกระบวนการติดตั้ง
- iOS และ App Store: Apple App Store จะไม่ถ่ายโอนพารามิเตอร์คิวรีของหน้าแลนดิ้งเพจเว็บตามอำเภอใจไปยังไบนารีแอปพลิเคชันที่ติดตั้งใหม่โดยอัตโนมัติ Universal Links รองรับการนำทางโดยตรงเมื่อมีการติดตั้งแอปพลิเคชันแล้วและกำหนดค่าโดเมนที่เกี่ยวข้องถูกต้อง แต่ไม่ได้การรับประกันการกู้คืนพารามิเตอร์แบบดีเฟอร์รีดด้วยตนเองหลังจากการติดตั้งใหม่
- Attribution SDKs: ใช้เฉพาะกลไกการกู้คืนที่ระบุไว้ในเอกสารสำหรับเวอร์ชัน SDK และแพลตฟอร์มที่กำลังใช้งานจริง ตรวจสอบการใช้งาน การเก็บรวบรวมข้อมูล และผลกระทบด้านความเป็นส่วนตัว แทนที่จะสมมติว่า SDK สามารถกู้คืนพารามิเตอร์การนำทางทุกตัวได้ในทุกเบราว์เซอร์และทุกเส้นทางการติดตั้ง
การระบุแหล่งที่มาของ iOS และขอบเขตความเป็นส่วนตัว: การปฏิบัติตามกฎการป้องกันลายนิ้วมืออุปกรณ์ของ Apple
การใช้งานการนำทางและการกู้คืนฉากแบบดีเฟอร์รีดบน iOS จะต้องไม่พึ่งพาการทำลายนิ้วมืออุปกรณ์ (Device Fingerprinting) ที่ถูกห้าม เพื่อระบุตัวตนของผู้ใช้หรืออุปกรณ์ข้ามแอปพลิเคชันหรือเว็บไซต์ ภายใต้ คำแนะนำเรื่องความเป็นส่วนตัวของผู้ใช้และการใช้ข้อมูลของ Apple (Apple User Privacy and Data Use Guidance) และข้อตกลงใบอนุญาตโครงการนักพัฒนาของ Apple (Apple Developer Program License Agreement) นักพัฒนาถูกห้ามอย่างเด็ดขาดในการดึงตัวระบุที่ไม่ซ้ำกันจากสัญญาณอุปกรณ์ เบราว์เซอร์ การกำหนดค่า ตำแหน่ง หรือเครือข่ายเพื่อวัตถุประสงค์นี้ การให้สิทธิ์การโปร่งใสในการติดตามแอป (ATT) ไม่ได้ทำให้การทำลายนิ้วมือที่ถูกห้ามกลายเป็นสิ่งที่อนุญาตให้ทำได้
ใช้ Universal Links สำหรับการนำทางแอปโดยตรงที่ได้รับการตรวจสอบแล้ว เมื่อติดตั้งแอปพลิเคชันแล้ว สำหรับการกู้คืนฉากหลังการติดตั้งบน iOS ให้ใช้กลไกที่บันทึกไว้ซึ่งพฤติกรรมได้รับการตรวจสอบสำหรับเวอร์ชัน SDK ที่ใช้งานและข้อกำหนดของ Apple ที่เกี่ยวข้อง ตรวจสอบการเปิดเผยข้อมูลความเป็นส่วนตัวของ SDK และการประกาศ API ตามเหตุผลที่กำหนด (ถ้ามี) แต่ต้องไม่ถือว่าการมีอยู่ของ Manifest ความความเป็นส่วนตัว (PrivacyInfo.xcprivacy) เป็นหลักฐานยืนยันว่าการใช้งาน Attribution เป็นไปตามข้อกำหนด
หาก SDK ที่เลือกพึ่งพาเมธอดการจับคู่ที่ไม่สามารถยืนยันได้ว่าได้รับอนุญาต ห้ามแนะนำเมธอดนั้นสำหรับการใช้งานบน iOS ในกรณีที่ไม่มีกลไกที่ได้รับอนุญาตใดสามารถสร้างบริบทการนำทางแบบดีเฟอร์รีดได้อย่างน่าเชื่อถือ ให้รักษาขั้นตอนการเริ่มต้นใช้งานตามปกติ พาผู้ใช้ไปยังหน้าจอหลักเริ่มต้นที่ปลอดภัย และทำเครื่องหมายการติดตั้งนั้นว่าไม่สามารถระบุแหล่งที่มาได้ แทนที่จะสร้างการจับคู่ที่ยังไม่ได้รับการตรวจสอบขึ้นมาเอง
การแสดงภาพกระบวนการกู้คืนฉาก QR แบบสองสถานะ
แผนผังSสถาปัตยกรรมด้านล่างแสดงขั้นตอนต่าง ๆ เมื่อ QR code ที่มีพารามิเตอร์แตกแขนงไปสู่การปลุกการทำงานของ OS โดยตรงหรือการกู้คืนผ่านสโตร์แบบดีเฟอร์รีด ก่อนที่จะมารวมกันที่ตัวตรวจสอบเส้นทางฝั่งไคลเอนต์และเกตการตรวจสอบสิทธิ์:
[User Scans QR Code] ──> URL (?route=product_detail&id=sku_8842&channelCode=pos_01)
│
┌────┴────────────────────────────────────────┐
▼ (App Already Installed) ▼ (App Not Installed)
[OS Universal Link / App Link] [Intermediary H5 Landing Page]
│ │
▼ ▼
[Hot/Cold Wakeup Callback] [Store Download & First Launch]
(onNewIntent / continueUserActivity) (Platform-Supported Deferred Recovery)
│ │
└────────────────────┬────────────────────────┘
▼
[Client Route Validator & Auth Gate]
│
┌────────────────────┴────────────────────────┐
▼ (Allowlisted & Authenticated) ▼ (Invalid Route / Unmatched)
[Navigate to Product Detail Screen] [Fallback to Default Home Screen]
ขั้นตอนที่ 1: การออกแบบสคีมาพารามิเตอร์การนำทางแบบกำหนดเอง
การสร้างโครงสร้างเพย์โหลดการนำทางที่ใช้งานได้เทียบกับแท็กการระบุแหล่งที่มาแบบ passive
เมื่อสร้าง QR code สำหรับการนำทางในแอป ทีมวิศวกรรมต้องสร้างสัญญาข้อมูลสองชั้น (Two-tier data contract) ที่สะอาด ซึ่งแยก คีย์คิวรี URL ภายนอกแบบกะทัดรัด ออกจาก เพย์โหลด JSON ภายในที่ผ่านการ normalize แล้ว:
| ระดับสัญญา (Contract Tier) | ข้อตกลงการตั้งชื่อฟิลด์ | ตัวอย่างการแสดงผล | วัตถุประสงค์ทางสถาปัตยกรรม |
|---|---|---|---|
| ระดับคิวรี URL ภายนอก | route, id, channelCode |
?route=product_detail&id=sku_8842&channelCode=pos_01 |
ลดความยาวอักขระ URI เพื่อให้เมทริกซ์ QR ที่พิมพ์ออกมามีขนาดกะทัดรัด |
| ระดับ normalize ภายใน | route_key, target_id, channel_code, fallback_route |
{"route_key": "product_detail", "target_id": "sku_8842", ...} |
ออบเจกต์มาตรฐานที่เป็นทางการซึ่งได้รับการตรวจสอบโดย H5 และตัวควบคุมเส้นทางฝั่งไคลเอนต์เนทีฟ |
| รีจิสทรีความปลอดภัยของไคลเอนต์ | PROTECTED_ROUTES (ค่าคงที่ภายใน) |
setOf("room_lobby", "promo_campaign") |
กำหนดข้อกำหนดการตรวจสอบสิทธิ์ภายใน ไม่อนุญาตให้เชื่อถือจาก URL ภายนอกเด็ดขาด |

ในโมเดลสองชั้นนี้:
- แท็กการระบุแหล่งที่มาแบบ passive (
channelCodechannel_code) จะถูกใช้โดยไปพไลน์การวิเคราะห์เพื่อวัดปริมาณการแปลงทางการตลาด (Conversion volume) โดยไม่ต้องเปลี่ยนสถานะ UI ของไคลเอนต์ - พารามิเตอร์การนำทางที่ใช้งานได้ (
routeroute_keyและidtarget_id) จะสั่งการตัวควบคุมการนำทางของไคลเอนต์เนทีฟว่าควรแสดงหน้าจอใดและดึงข้อมูลเอนทิตีใด - การแยกนโยบายการตรวจสอบสิทธิ์: การพิจารณาว่าเส้นทางใดต้องการการตรวจสอบสิทธิ์ (
requires_auth) จะต้องอ้างอิงจากรีจิสทรีเส้นทางที่เชื่อถือได้ภายในของแอปพลิเคชันไคลเอนต์โดยตรง แทนที่จะยอมรับจากพารามิเตอร์คิวรี QR ภายนอก
การบังคับใช้การเข้ารหัสร้อยละตามมาตรฐาน IETF RFC 3986 และข้อจำกัดขนาดเพย์โหลด
พารามิเตอร์การนำทางทั้งหมดที่ผนวกเข้ากับ URL ปลายทางของ QR จะต้องเป็นไปตามไวยากรณ์ทั่วไปของ URI ที่ระบุไว้ใน IETF RFC 3986:
- หลีกเลี่ยงตัวคั่นแบบซ้อน (Nested Delimiters): ห้ามฝังสตริง JSON ดิบ (เช่น
?data={"route":"product","id":123}) ลงในสตริงคิวรี URL โดยตรง วงเล็บ เครื่องหมายคำพูด และเครื่องหมายทวิภาคที่ไม่ได้หลีกเลี่ยง (unescaped) มักจะทำให้ตัวแยกวิเคราะห์ URL ของเบราว์เซอร์ในแอปขัดข้อง และเพิ่มความหนาแน่นของโมดูลบาร์โค้ด - ทำให้คู่คีย์-ค่าแบนราบ (Flatten Key-Value Pairs): แสดงบริบทการนำทางภายนอกเป็นพารามิเตอร์คิวรีแบบแบนที่เข้ารหัสเปอร์เซ็นต์ (เช่น
https://promo.example.com/open?route=product_detail&id=sku_8842&channelCode=pos_01) - บังคับใช้ขอบเขตอักขระที่เข้มงวด: จำกัดค่า
routeและidให้อยู่ในอักขระตัวอักษรและตัวเลข ยัติภังค์ และขีดล่างที่ปลอดภัยสำหรับ URL (^[A-Za-z0-9_-]{1,64}$)
การกำหนดสัญญา Allowlist เส้นทางฝั่งไคลเอนต์
เมื่อรับพารามิเตอร์ URL ภายนอกและแปลงให้อยู่ในโครงสร้างเพย์โหลดภายในแล้ว พารามิเตอร์เหล่านั้นจะต้องสอดคล้องกับสัญญาคลาสสิกที่เข้มงวด หากไม่รู้จัก route_key หรือหากเส้นทางเฉพาะของเอนทิตี (product_detail, room_lobby, promo_campaign) ขาด target_id ที่จำเป็น ตัวแปลงมาตรฐานจะต้องลดระดับ (downgrade) เพย์โหลดไปเป็น home_default โดยมี target_id: null
JSON Schema ด้านล่างกำหนดโครงสร้างที่เป็นทางการสำหรับเพย์โหลดการกู้คืนฉากที่ผ่านการ normalize แล้ว:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "QrSceneRestorationPayload",
"description": "Canonical, normalized routing payload. Authentication policy is derived separately from the application's trusted route registry.",
"type": "object",
"additionalProperties": false,
"required": [
"schema_version",
"route_key",
"target_id",
"fallback_route"
],
"properties": {
"schema_version": {
"const": "1.0.0"
},
"route_key": {
"type": "string",
"enum": [
"product_detail",
"room_lobby",
"promo_campaign",
"home_default"
]
},
"target_id": {
"type": ["string", "null"],
"pattern": "^[A-Za-z0-9_-]{1,64}$"
},
"channel_code": {
"type": "string",
"pattern": "^[A-Za-z0-9_.-]{1,64}$"
},
"fallback_route": {
"const": "home_default"
}
},
"allOf": [
{
"if": {
"properties": {
"route_key": {
"enum": [
"product_detail",
"room_lobby",
"promo_campaign"
]
}
},
"required": ["route_key"]
},
"then": {
"properties": {
"target_id": {
"type": "string"
}
}
}
},
{
"if": {
"properties": {
"route_key": {
"const": "home_default"
}
},
"required": ["route_key"]
},
"then": {
"properties": {
"target_id": {
"type": "null"
}
}
}
}
],
"examples": [
{
"schema_version": "1.0.0",
"route_key": "product_detail",
"target_id": "sku_8842",
"channel_code": "pos_01",
"fallback_route": "home_default"
}
]
}
ขั้นตอนที่ 2: การจับภาพและการส่งต่อบริบทการนำทางบนหน้าแลนดิ้งเพจ
การแยกวิเคราะห์คีย์การนำทางที่อยู่ใน Allowlist บนหน้าแลนดิ้งเพจ H5
เมื่อผู้ใช้ที่ยังไม่ได้ติดตั้งแอปพลิเคชันสแกน QR code เบราว์เซอร์มือถือจะเปิดหน้าแลนดิ้งเพจ H5 ตัวกลางที่โฮสต์ไว้ สคริปต์ของหน้าแลนดิ้งเพจจะดึงคีย์คิวรีภายนอก (route, id, channelCode) ปฏิเสธพารามิเตอร์ซ้ำที่คลุมเครือ และทำการ normalize ให้เป็นโครงสร้างมาตรฐานที่กำหนดไว้ในขั้นตอนที่ 1
ตัวแปลง JavaScript ด้านล่างจะบังคับใช้ความสำคัญของคีย์เดี่ยว ขอบเขต RegEx และเงื่อนไขการลดระดับเป็น home_default ในขณะที่ถือว่าพารามิเตอร์สงวนที่ซ้ำกันไม่ถูกต้อง:
const query = new URLSearchParams(window.location.search);
const routeValues = query.getAll("route");
const idValues = query.getAll("id");
const channelValues = query.getAll("channelCode");
const allowedRoutes = new Set([
"product_detail",
"room_lobby",
"promo_campaign",
"home_default"
]);
const idPattern = /^[A-Za-z0-9_-]{1,64}$/;
const channelPattern = /^[A-Za-z0-9_.-]{1,64}$/;
function normalizeQrRoutingPayload() {
// Reject duplicate keys to defend against parameter pollution
if (routeValues.length > 1 || idValues.length > 1 || channelValues.length > 1) {
return {
schema_version: "1.0.0",
route_key: "home_default",
target_id: null,
fallback_route: "home_default"
};
}
const rawRoute = routeValues.length === 1 ? routeValues[0] : null;
const rawId = idValues.length === 1 ? idValues[0] : null;
const rawChannel = channelValues.length === 1 ? channelValues[0] : null;
let routeKey = allowedRoutes.has(rawRoute)
? rawRoute
: "home_default";
let targetId =
rawId && idPattern.test(rawId)
? rawId
: null;
if (routeKey === "home_default" || targetId === null) {
routeKey = "home_default";
targetId = null;
}
const payload = {
schema_version: "1.0.0",
route_key: routeKey,
target_id: targetId,
fallback_route: "home_default"
};
if (rawChannel && channelPattern.test(rawChannel)) {
payload.channel_code = rawChannel;
}
return payload;
}
const routingPayload = normalizeQrRoutingPayload();
การบูรณาการ Web SDK ผ่านอะแดปเตอร์ที่ตรวจสอบเวอร์ชันแล้ว
หน้าแลนดิ้งเพจประสานงานทั้งความพยายามปลุกแอปโดยทันทีและการเตรียมพารามิเตอร์แบบดีเฟอร์รีด ก่อนที่จะปรับใช้เว็บ Attribution SDK (เช่น opoinstall.js) ให้ยืนยัน URL ของบันเดิล JavaScript, ลายเซ็นการเริ่มต้นใช้งาน, การเรียกกลับความพร้อม (Readiness callback), วิธีการปลุก และสคีมาพารามิเตอร์ กับเอกสารอย่างเป็นทางการสำหรับเวอร์ชัน SDK ที่กำลังใช้งาน
เก็บพฤติกรรมเฉพาะของ SDK ไว้เบื้องหลังอะแดปเตอร์แอปพลิเคชันขนาดเล็กที่ยอมรับออบเจกต์ routingPayload ที่ผ่านการ normalize แล้ว สำหรับข้อกำหนดโดยละเอียดเกี่ยวกับการบูรณาการเว็บและการกำหนดค่าพารามิเตอร์ช่องทาง โปรดศึกษา เอกสารสำหรับนักพัฒนาของ OpoInstall
การรักษาพารามิเตอร์การนำทางข้ามผ่านลิงก์สำรองสโตร์โดยตรง
หาก Web JS SDK ไม่สามารถใช้งานได้หรือถูกบล็อกโดยการควบคุมความปลอดภัยของเบราว์เซอร์ หน้าแลนดิ้งเพจจะสำรองข้อมูลไปยังลิงก์เปลี่ยนเส้นทางสโตร์โดยตรง บนการใช้งาน Android ที่ใช้ Google Play ลิงก์สำรองสามารถเข้ารหัสคีย์การนำทางที่ผ่านการ normalize แล้วลงในพารามิเตอร์ referrer ของ Play Store เพื่อให้ไคลเอนต์เนทีฟสามารถกู้คืนบริบทการนำทางผ่าน Google Play Install Referrer API เมื่อติดตั้งผ่านกระบวนการ Play Store อย่างเป็นทางการ:
// Example Android Play Store fallback URL preserving normalized routing context
const referrerString = `route_key=${routingPayload.route_key}&target_id=${routingPayload.target_id || ""}&channel_code=${routingPayload.channel_code || ""}`;
const playStoreFallbackUrl = `https://play.google.com/store/apps/details?id=com.example.app&referrer=${encodeURIComponent(referrerString)}`;
ขั้นตอนที่ 3: การใช้งานการส่งต่อเส้นทางไคลเอนต์เนทีฟและการกู้คืนฉาก
การจัดการโหมดการเปิดใช้งาน Activity ของ Android: เหตุใด singleTask จึงต้องใช้ทั้ง onCreate และ onNewIntent
ในสถาปัตยกรรมลิงก์เชิงลึกของ Android วิธีที่ Activity รับ Intent การนำทางจะขึ้นอยู่กับการกำหนดค่าโหมดการเปิดใช้งานและการออกแบบกราฟการนำทาง นักพัฒนาอาจใช้โหมดการเปิดใช้งานมาตรฐานกับ Jetpack Navigation, singleTop หรือ singleTask ขึ้นอยู่กับข้อกำหนดของสแต็กงาน (Task stack)
เมื่อ Activity ถูกกำหนดค่าด้วย singleTop หรือ singleTask เพื่อป้องกันไม่ให้อินสแตนซ์ที่ซ้ำกันซ้อนกันระหว่างการปลุกด้วยลิงก์เชิงลึก จะต้องจัดการเส้นทางวงจรชีวิตที่แตกต่างกันสองเส้นทาง:
- การเริ่มต้นแบบเย็น (Cold Start -
onCreate): เมื่อกระบวนการของแอปพลิเคชันไม่ได้ทำงานอยู่ การสแกน QR code จะเปิด Activity ตั้งแต่เริ่มต้น Intent ขาเข้าจะถูกส่งไปยังonCreate(savedInstanceState: Bundle?) - การเริ่มต้นแบบอุ่น (Warm Start -
onNewIntent): เมื่อระบบปฏิบัติการนำอินสแตนซ์ Activity ที่มีอยู่กลับมาใช้ใหม่ภายใต้โหมดการเปิดใช้งานและเงื่อนไขสแต็กงานที่กำหนดค่าไว้ Intent ของลิงก์เชิงลึกขาเข้าจะถูกส่งไปยังonNewIntent()ภายใต้singleTopการนำกลับมาใช้ใหม่จะเกิดขึ้นก็ต่อเมื่อ Activity เป้าหมายอยู่ที่ด้านบนสุดของสแต็กงานที่เกี่ยวข้องแล้วเท่านั้น มิฉะนั้น อาจมีการสร้างอินสแตนซ์ Activity ใหม่ ให้ดำเนินการจัดการทั้งonCreate()และonNewIntent()และตรวจสอบพฤติกรรมจริงสำหรับสถาปัตยกรรมกาารนำทางของแอปพลิเคชัน
เพื่อให้จัดการสถานะทั้งสองได้อย่างน่าเชื่อถือโดยไม่บล็อกผู้ใช้ที่สแกน QR code เดิมซ้ำอย่างถูกต้องตามกฎหมายในเซสชันต่อมา Activity ควรทำเครื่องหมายอินสแตนซ์ Intent ที่ประมวลผลแล้วแต่ละรายการด้วยแฟล็กการบริโภค (EXTRA_ROUTE_CONSUMED) และเพิกเฉยต่อ Intent ที่ส่งซ้ำผ่านการเปิดจากตัวสลับแอปในอดีตเท่านั้น (Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY)
การจัดการจุดเข้าสู่วงจรชีวิตของ iOS: AppDelegate เทียบกับกิจกรรมการต่อเนื่องของ UISceneDelegate
บน iOS การนำทาง Universal Link โดยตรงและการดึงข้อมูลพารามิเตอร์แบบดีเฟอร์รีด จะต้องสอดคล้องกับวงจรชีวิต UI ที่กำหนดค่าไว้ของแอปพลิเคชัน:
- วงจรชีวิตแบบอิงซีน (Scene-Based Lifecycle -
UISceneDelegate): ในแอปพลิเคชันที่นำUISceneมาใช้ (มาตรฐานตั้งแต่ iOS 13 เป็นต้นไป) Universal Links แบบเปิดใช้งานเย็นจะมาถึงในscene(_:willConnectTo:options:)ผ่านconnectionOptions.userActivitiesในขณะที่ Universal Links แบบคืนค่าความพร้อมแบบอุ่นจะมาถึงในscene(_:continue:) - วงจรชีวิตแบบ Application-Delegate (
UIApplicationDelegate): ในแอปพลิเคชันที่ไม่ใช้ตัวแทนซีน Universal Links จะถูกส่งไปยังapplication(_:continue:restorationHandler:) - การกู้คืนการเปิดใช้ครั้งแรกแบบดีเฟอร์รีด (Deferred First-Launch Recovery): โดยไม่ขึ้นกับตัวแทนความต่อเนื่องของ Universal Link การคิวรีพารามิเตอร์การติดตั้งแบบดีเฟอร์รีดจะทำงานหนึ่งครั้งในระหว่างการเริ่มต้นแอปพลิเคชันครั้งแรก หลังจากลำดับชั้นมุมมองราก (Root view hierarchy) พร้อมแล้ว
นักพัฒนาสามารถดาวน์โหลดแพ็กเกจ SDK ของแพลตฟอร์มและตรวจสอบเทมเพลตการบูรณาการวงจรชีวิตผ่าน ศูนย์ดาวน์โหลด SDK ของ OpoInstall
การรวมพารามิเตอร์การปลุกโดยตรงและพารามิเตอร์ดีเฟอร์รีดการเปิดใช้ครั้งแรกเข้าสู่ตัวควบคุมเส้นทางเดียว
เพื่อรักษาความปลอดภัยที่สอดคล้องกันและพฤติกรรมการนำทาง สถาปัตยกรรมของไคลเอนต์จะ normalize ทั้ง URI การปลุกโดยตรงและเพย์โหลด JSON การติดตั้งแบบดีเฟอร์รีดให้อยู่ในโมเดล NavigationTarget เดียว ก่อนที่จะส่งต่อไปยังเราเตอร์ UI:
- การตรวจสอบแหล่งกำเนิด (Direct Intents): ตรวจสอบว่า URI ของ
Intent.ACTION_VIEWขาเข้าใช้สกีมาhttpsตรงกับโฮสต์ที่เกี่ยวข้องที่ได้รับการตรวจสอบแล้วของแอปพลิเคชัน (promo.example.com) และพาธ (/open) และไม่มีคิวรีคีย์ซ้ำที่ขัดแย้งกัน - การ Normalize สคีมาและการตรวจสอบ Allowlist: แมปคีย์
route/idภายนอกหรือฟิลด์route_key/target_idภายในเข้ากับโมเดลทางการ โดยปฏิเสธเส้นทางที่ไม่อยู่ใน allowlist หรือ ID ที่จัดรูปแบบไม่ถูกต้อง โปรดทราบว่าตรรกะการแยกวิเคราะห์ Kotlin ของไคลเอนต์ทำหน้าที่เป็นโครงร่างการแยกวิเคราะห์เชิงป้องกันที่เป็นตัวอย่าง ในระบบโปรดักชัน ให้แยกวิเคราะห์เพย์โหลดเทียบกับกฎสคีมาทางการทั้งหมดก่อนทำการส่งต่อ - ตัวป้องกันการเข้ามาซ้ำแบบ Single-Flight: ใช้ตัวป้องกันแบบอะตอมิกต่อการส่งมอบ เพื่อไม่ให้การเรียกกลับแบบพร้อมกันทริกเกอร์ธุรกรรมแฟรกเมนต์ที่ทับซ้อนกัน
การจัดการเกตการตรวจสอบสิทธิ์: การคิวเส้นทางลิงก์เชิงลึกที่รอดำเนินการจนกว่าการเข้าสู่ระบบของผู้ใช้จะเสร็จสมบูรณ์

เมื่อผู้ใช้ใหม่ติดตั้งแอปพลิเคชันจาก App Store ผ่าน QR code และเปิดใช้งานเป็นครั้งแรก โดยทั่วไปผู้ใช้จะยังไม่ได้เข้าสู่ระบบ หาก QR code มุ่งเป้าไปที่หน้าจอที่ได้รับการป้องกัน (เช่น room_lobby หรือ promo_campaign) และเราเตอร์พยายามนำทางทันที เกตการตรวจสอบสิทธิ์จะเปลี่ยนเส้นทางผู้ใช้ไปยังหน้าจอเข้าสู่ระบบและสูญเสียบริบทของลิงก์เชิงลึก
เพื่อรักษากู้คืนฉากข้ามขอบเขตการตรวจสอบสิทธิ์:
- การค้นหานโยบายเส้นทางภายใน:พิจารณาว่าเส้นทางนั้นต้องมีการตรวจสอบสิทธิ์หรือไม่โดยตรวจสอบรีจิสทรี
PROTECTED_ROUTESภายในของไคลเอนต์ แทนที่จะเชื่อถือแฟล็กเพย์โหลดภายนอก - การเตรียมการแบบอะตอมิก: หากผู้ใช้ยังไม่ได้เข้าสู่ระบบ ให้จัดเก็บ
NavigationTargetที่ผ่านการตรวจสอบแล้วในPendingRouteStoreที่มีขอบเขตเซสชันและเป็นอะตอมิก (AtomicReference) และนำทางผู้ใช้ไปยังขั้นตอนการเข้าสู่ระบบ โปรดทราบว่าหน่วยความจำภายในPendingRouteStoreจะใช้กับกระบวนการแอปพลิเคชันที่ใช้งานอยู่ หากขั้นตอนการตรวจสอบสิทธิ์สามารถขยายขอบเขตไปถึงการปิดกระบวนการ ให้ใช้ที่เก็บข้อมูลภายในเครื่องที่เข้ารหัสและจำกัดเวลา ซึ่งจะล้างสถานะเมื่อมีการสลับบัญชี - การตรวจสอบความถูกต้องซ้ำและการส่งต่อหลังการตรวจสอบสิทธิ์: เมื่อการเข้าสู่ระบบเสร็จสมบูรณ์ ให้ใช้เป้าหมายที่เตรียมไว้แบบอะตอมิก (
getAndSet(null)) ตรวจสอบว่าขณะนี้เซสชันได้รับการตรวจสอบสิทธิ์แล้ว ตรวจสอบพารามิเตอร์เส้นทางซ้ำ และดำเนินการเปลี่ยนผ่านหน้าจอ
ความพร้อมของโฮสต์การนำทางและการกู้คืนสถานะ
การเรียกกลับ onNavigationGraphReady() จะต้องเชื่อมต่อกับการเริ่มต้นใช้งานโฮสต์การนำทางจริงของแอปพลิเคชัน เมื่อกราฟการนำทางถูกกำหนดไว้ใน NavHostFragment ที่โฮสต์ด้วย XML กิจกรรมสามารถสร้างการเชื่อมต่อนี้ได้หลังจากโฮสต์และปลายทางพร้อมแล้ว หากมีการติดตั้งกราฟแบบอะซิงโครนัส ให้เรียกใช้การเรียกกลับหลังจากที่การเริ่มต้นกราฟเสร็จสมบูรณ์แล้วเท่านั้น
คำขอการนำทางที่รอดำเนินการซึ่งจัดเก็บไว้ในฟิลด์ Activity เพียงอย่างเดียวอาจสูญหายไประหว่างการเปลี่ยนแปลงการกำหนดค่า สำหรับการใช้งานในระบบโปรดักชัน ให้เก็บสถานะการนำทางที่ยังไม่ได้บริโภคไว้ใน ViewModel ที่ตระหนักถึงวงจรชีวิต (Lifecycle-aware) และใช้ SavedStateHandle เมื่อจำเป็นต้องกู้คืนข้ามการสร้าง Activity ใหม่ที่ขับเคลื่อนโดยระบบหรือการปิดกระบวนการ โค้ดที่แสดงที่นี่เป็นตัวอย่างเชิงสถาปัตยกรรมและต้องมีการนำทางเฉพาะแอปพลิเคชันและการบูรณาการเซสชันเพิ่มเติม
โค้ด Kotlin ด้านล่างมีโครงร่างการนำทางของ Android เป็นตัวอย่าง พร้อมด้วยอะแดปเตอร์การนำทางและ SDK ที่เป็นของแอปพลิเคชันซึ่งแสดงการควบคุมเหล่านี้:
<!-- Android Manifest Configuration for App Link Verification -->
<!-- File: app/src/main/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.app">
<application
android:allowBackup="true"
android:label="@string/app_name"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<activity
android:name=".SceneRestorationActivity"
android:exported="true"
android:launchMode="singleTask">
<!-- Verified Android App Link Intent Filter -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Origin must match /.well-known/assetlinks.json SHA-256 fingerprint -->
<data
android:scheme="https"
android:host="promo.example.com"
android:pathPrefix="/open" />
</intent-filter>
</activity>
</application>
</manifest>
// Android Routing Activity Implementation
// File: app/src/main/java/com/example/app/SceneRestorationActivity.kt
//
// INTEGRATION NOTE:
// This example is an architectural routing skeleton, not a drop-in production implementation.
// Bind AttributionRoutingAdapter and NavController initialization to your verified SDK release,
// session manager, and NavHostFragment before deployment.
package com.example.app
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.fragment.NavHostFragment
import org.json.JSONObject
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
/**
* Validated navigation target model representing an allowlisted in-app destination.
* Authentication requirement is derived strictly from the internal route registry.
*/
data class NavigationTarget(
val eventKey: String,
val routeKey: String,
val targetId: String?,
val requiresAuth: Boolean
)
/**
* Process-scoped atomic store holding a pending route when an unauthenticated user
* triggers a protected deep link during cold start or first-launch onboarding.
*/
object PendingRouteStore {
private val pending = AtomicReference<NavigationTarget?>(null)
fun stage(target: NavigationTarget) {
pending.set(target)
}
fun consume(): NavigationTarget? {
return pending.getAndSet(null)
}
}
class SceneRestorationActivity : AppCompatActivity() {
companion object {
private const val TAG = "SceneRestorationRouter"
private const val TRUSTED_HOST = "promo.example.com"
private const val TRUSTED_PATH = "/open"
private const val EXTRA_ROUTE_CONSUMED = "com.example.app.EXTRA_ROUTE_CONSUMED"
private const val ROUTE_HOME = "home_default"
private const val ROUTE_PRODUCT = "product_detail"
private const val ROUTE_LOBBY = "room_lobby"
private const val ROUTE_PROMO = "promo_campaign"
private val ALLOWED_ROUTES = setOf(ROUTE_HOME, ROUTE_PRODUCT, ROUTE_LOBBY, ROUTE_PROMO)
private val PROTECTED_ROUTES = setOf(ROUTE_LOBBY, ROUTE_PROMO)
private val SAFE_ID_REGEX = Regex("^[A-Za-z0-9_-]{1,64}$")
// Process-level guard ensuring deferred install recovery is queried only once per cold start
private val hasQueriedDeferredInstall = AtomicBoolean(false)
}
// Per-delivery re-entrancy guard; allows legitimate subsequent scans of the same QR code
private val processingIntent = AtomicBoolean(false)
// Staging container for routes that arrive before the UI/NavController is mounted
private val pendingUiTarget = AtomicReference<NavigationTarget?>(null)
private var navigationReady = false
// Application-owned adapter isolating vendor SDK and session calls
private val attributionAdapter = AttributionRoutingAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_scene_restoration)
// Connect NavHost initialization when defined in layout
connectNavigationHost()
// Activity recreation (e.g., screen rotation) is not a new install or new QR scan
if (savedInstanceState != null) {
return
}
val launchIntent = intent
val isDirectLinkLaunch =
launchIntent?.action == Intent.ACTION_VIEW &&
launchIntent.data != null
if (isDirectLinkLaunch) {
processIncomingIntentOnce(launchIntent) { candidate ->
handleIncomingWakeupIntent(candidate)
}
// Do not start deferred recovery after a direct-link launch,
// even if the direct route was rejected.
return
}
// Only initial normal application launches may request deferred recovery
if (hasQueriedDeferredInstall.compareAndSet(false, true)) {
queryDeferredSceneParameters()
}
}
/**
* Connect this method to the actual NavHost initialization.
* Uses currentDestination check and destination listener to safely verify graph readiness
* without throwing IllegalStateException on unset controller.graph.
*/
private fun connectNavigationHost() {
val navHost = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as? NavHostFragment
if (navHost == null) {
Log.w(TAG, "NavHost not attached; route remains queued.")
// For an asynchronously attached NavHost, call this
// method again after the host has been attached.
return
}
val controller = navHost.navController
// currentDestination is nullable before a destination has been established.
// Do not access controller.graph merely to check whether setGraph() has been called.
if (controller.currentDestination != null) {
onNavigationGraphReady()
return
}
// If the graph/destination becomes available later,
// mark navigation ready when a destination is established.
val listener = object : androidx.navigation.NavController.OnDestinationChangedListener {
override fun onDestinationChanged(
navController: androidx.navigation.NavController,
destination: androidx.navigation.NavDestination,
arguments: Bundle?
) {
navController.removeOnDestinationChangedListener(this)
onNavigationGraphReady()
}
}
controller.addOnDestinationChangedListener(listener)
}
/**
* Captures warm-start QR scans when the Activity is reused under singleTop or singleTask.
*/
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
processIncomingIntentOnce(intent) { candidate ->
handleIncomingWakeupIntent(candidate)
}
}
private fun processIncomingIntentOnce(incomingIntent: Intent?, process: (Intent) -> Unit) {
val candidate = incomingIntent ?: return
// Ignore historical intents re-delivered from recent-apps switcher or configuration changes
if ((candidate.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) != 0) return
if (candidate.getBooleanExtra(EXTRA_ROUTE_CONSUMED, false)) return
if (!processingIntent.compareAndSet(false, true)) return
try {
process(candidate)
candidate.putExtra(EXTRA_ROUTE_CONSUMED, true)
} finally {
processingIntent.set(false)
}
}
private fun handleIncomingWakeupIntent(incomingIntent: Intent?): Boolean {
if (incomingIntent?.action != Intent.ACTION_VIEW) return false
val uri: Uri = incomingIntent.data ?: return false
// 1. Verify trusted HTTPS origin and path
val isTrusted =
uri.scheme.equals("https", ignoreCase = true) &&
uri.host.equals(TRUSTED_HOST, ignoreCase = true) &&
uri.path == TRUSTED_PATH
if (!isTrusted) {
Log.w(TAG, "Rejected untrusted deep-link origin: $uri")
return false
}
// 2. Reject missing or conflicting duplicate query parameters
if (uri.getQueryParameters("route").size != 1 ||
uri.getQueryParameters("id").size != 1) {
Log.w(TAG, "Rejected missing or duplicate routing keys in URI")
return false
}
val rawRoute = uri.getQueryParameter("route")
val rawId = uri.getQueryParameter("id")
val target = validateAndBuildTarget("wakeup", rawRoute, rawId)
if (target.routeKey != ROUTE_HOME) {
dispatchNavigationTarget(target)
return true
}
return false
}
private fun queryDeferredSceneParameters() {
// Application-defined timeout configuration; tune for deployed service behavior
attributionAdapter.fetchDeferredInstallParams(timeoutMs = 5000L) { jsonPayload ->
if (jsonPayload == null) {
Log.d(TAG, "No deferred routing parameters recovered; remaining on default view.")
return@fetchDeferredInstallParams
}
val target = parseJsonRoutingPayload("deferred_install", jsonPayload)
runOnUiThread {
dispatchNavigationTarget(target)
}
}
}
private fun parseJsonRoutingPayload(sourcePrefix: String, rawJson: String): NavigationTarget {
return try {
val obj = JSONObject(rawJson)
// Defensive normalization: map canonical 'route_key'/'target_id' with external 'route'/'id' fallback
val rawRoute = obj.optString("route_key", "").ifEmpty { obj.optString("route", "") }
val rawId = if (obj.has("target_id") && !obj.isNull("target_id")) {
obj.optString("target_id", "")
} else {
obj.optString("id", "")
}
validateAndBuildTarget(sourcePrefix, rawRoute, rawId)
} catch (e: Exception) {
Log.w(TAG, "Malformed routing JSON payload: ${e.message}")
NavigationTarget(sourcePrefix, ROUTE_HOME, null, false)
}
}
/**
* Enforces strict allowlist and regex bounds on untrusted routing parameters.
*/
private fun validateAndBuildTarget(
sourcePrefix: String,
rawRoute: String?,
rawId: String?
): NavigationTarget {
val cleanRoute = if (rawRoute != null && ALLOWED_ROUTES.contains(rawRoute)) {
rawRoute
} else {
ROUTE_HOME
}
if (cleanRoute == ROUTE_HOME) {
return NavigationTarget(sourcePrefix, ROUTE_HOME, null, false)
}
// Entity-specific routes strictly require a valid, sanitized targetId
val cleanId = if (rawId != null && SAFE_ID_REGEX.matches(rawId)) {
rawId
} else {
null
}
if (cleanId == null) {
Log.w(TAG, "Route $cleanRoute rejected due to missing or invalid target_id.")
return NavigationTarget(sourcePrefix, ROUTE_HOME, null, false)
}
val needsAuth = PROTECTED_ROUTES.contains(cleanRoute)
return NavigationTarget("${sourcePrefix}_${cleanRoute}_${cleanId}", cleanRoute, cleanId, needsAuth)
}
/**
* Invoked when the application's NavHost / NavController graph is attached and ready.
*/
fun onNavigationGraphReady() {
navigationReady = true
flushPendingNavigation()
}
private fun flushPendingNavigation() {
if (!navigationReady || isFinishing || isDestroyed) return
val target = pendingUiTarget.getAndSet(null) ?: return
dispatchReadyNavigationTarget(target)
}
private fun dispatchNavigationTarget(target: NavigationTarget) {
if (isFinishing || isDestroyed) return
// Stage route if the UI navigation graph has not finished inflating
if (!navigationReady) {
pendingUiTarget.set(target)
return
}
dispatchReadyNavigationTarget(target)
}
private fun dispatchReadyNavigationTarget(target: NavigationTarget) {
if (target.routeKey == ROUTE_HOME) {
navigateToHomeScreen()
return
}
// Check authentication gate for protected destinations
if (target.requiresAuth && !attributionAdapter.isUserAuthenticated()) {
Log.d(TAG, "Protected route ${target.routeKey} staged pending user authentication.")
PendingRouteStore.stage(target)
navigateToLoginScreen()
return
}
executeScreenTransition(target.routeKey, target.targetId)
}
/**
* Invoked by the authentication controller after login or registration completes.
*/
fun onUserAuthenticationCompleted() {
val pending = PendingRouteStore.consume()
if (pending == null) {
navigateToHomeScreen()
return
}
if (isFinishing || isDestroyed) return
if (!attributionAdapter.isUserAuthenticated()) {
PendingRouteStore.stage(pending)
navigateToLoginScreen()
return
}
val validated = validateAndBuildTarget(
pending.eventKey,
pending.routeKey,
pending.targetId
)
if (validated.routeKey == ROUTE_HOME) {
navigateToHomeScreen()
return
}
executeScreenTransition(validated.routeKey, validated.targetId)
}
private fun executeScreenTransition(routeKey: String, targetId: String?) {
// Implementation: perform backend-authorized resource fetch and invoke NavController.navigate()
Log.d(TAG, "Dispatching verified screen transition: route=$routeKey, id=$targetId")
}
private fun navigateToLoginScreen() {
// Implementation: transition to LoginFragment / LoginActivity
}
private fun navigateToHomeScreen() {
// Implementation: display default home screen
}
}
/**
* Application-owned adapter skeleton. Replace stubbed methods with verified OpoInstall SDK
* callbacks, Google Play Install Referrer queries, and your production session manager.
*/
class AttributionRoutingAdapter {
fun fetchDeferredInstallParams(timeoutMs: Long, callback: (String?) -> Unit) {
// Bind to verified SDK install parameter API or Play Install Referrer in production
callback(null)
}
fun isUserAuthenticated(): Boolean {
// Bind to production authentication session state
return false
}
}
ความปลอดภัยและการวินิจฉัย: การป้องกันการฉีดเส้นทางและการนำทางหลุด
การป้องกันการฉีดเส้นทางผ่านลิงก์เชิงลึกและการโจมตีแบบ Open-Redirect ตามแนวทางของ OWASP
พารามิเตอร์ที่ดึงมาจาก QR code หรือการเรียกกลับของการระบุแหล่งที่มาแบบดีเฟอร์รีด แสดงถึงอินพุตภายนอกที่ไม่น่าเชื่อถือ เพื่อให้สอดคล้องกับ มาตรฐานการตรวจสอบความปลอดภัยของแอปพลิเคชันมือถือ OWASP (OWASP Mobile Application Security Verification Standard) และ ความเสี่ยงด้านความปลอดภัยและแนวทางปฏิบัติสำหรับมือถือ 10 อันดับแรกของ OWASP (OWASP Mobile Top 10 Security Risks and Guidelines) เราเตอร์ของไคลเอนต์จะต้องบังคับใช้ขอบเขตการป้องกันสามประการ:
- ห้ามการโหลด URL ของ WebView ตามอำเภอใจ: ไม่อนุญาตให้พารามิเตอร์การนำทาง (เช่น
?route=webview&url=https://...) โหลด URL ภายนอกที่ยังไม่ได้ตรวจสอบภายใน WebView ของแอปพลิเคชันเด็ดขาด ผู้โจมตีสามารถเข้ารหัส URL ฟิชชิ่งลงใน QR code เพื่อหาประโยชน์จากเฟรม UI เนทีฟของแอปพลิเคชันในการเก็บเกี่ยวข้อมูลประจำตัว - ปฏิเสธมลพิษจากพารามิเตอร์ซ้ำ (Duplicate Parameter Pollution): หาก URL ที่ถูกจัดการมีคีย์ที่ขัดแย้งกันหลายคีย์ (เช่น
?route=home_default&route=room_lobby) ให้ปฏิเสธ URI แทนที่จะปล่อยให้ตัวแยกวิเคราะห์เลือกองค์ประกอบแรกหรือองค์ประกอบสุดท้ายตามอำเภอใจ - บังคับใช้การอนุญาตทรัพยากรฝั่งเซิร์ฟเวอร์: การส่ง
target_id=sku_8842จะสั่งการเฉพาะไคลเอนต์ว่าควรขอร้องมุมมองใด เมื่อหน้าจอขอข้อมูลจาก API ฝั่งเซิร์ฟเวอร์ เซิร์ฟเวอร์จะต้องตรวจสอบอย่างอิสระว่าเซสชันของผู้ใช้ปัจจุบันได้รับอนุญาตให้เข้าถึงเอนทิตีนั้นหรือไม่ เพื่อป้องกันช่องโหว่ Insecure Direct Object Reference (IDOR)
การวินิจฉัยสภาวะแข่งขันของการนำทางระหว่าง Cold-Start และ Warm-Start
ในระหว่างการเริ่มต้นแบบเย็น (Cold start) เจตนาลิงก์เชิงลึก (Deep-link intents) หรือการเรียกกลับ SDK ที่แคชไว้อาจได้รับการแก้ไขก่อนที่ NavHostFragment หลักหรือกราฟการนำทางรากจะขยายตัวเสร็จสิ้น การพยายามเรียกใช้ navController.navigate() ก่อนที่ลำดับชั้นมุมมองจะพร้อมจะทำให้เกิดข้อยกเว้นรันไทม์หรือการเปลี่ยนผ่านที่ถูกยกเลิก ควรเลื่อนการดำเนินการนำทางออกไปเสมอจนกว่าลำดับชั้นมุมมองโฮสต์จะถูกแนบและใช้งานอยู่ (!isFinishing && !isDestroyed)
การจัดการโปรโมชั่นที่หมดอายุ, SKU ที่ถูกลบ และพารามิเตอร์ที่หายไปอย่างราบรื่น
QR code ทางกายภาพมักจะยังคงติดตั้งอยู่ในร้านค้าปลีกหรือสื่อสิ่งพิมพ์หลังจากโปรโมชั่นดิจิทัลสิ้นสุดลงหรือ SKU ของสินค้าถูกยกเลิก เมื่อหน้าจอเป้าหมายดึงข้อมูลสำหรับ target_id และได้รับการตอบกลับ HTTP 404 Not Found หรือ 410 Gone จากฝั่งเซิร์ฟเวอร์ ไคลเอนต์จะต้องจับข้อผิดพลาด แสดงประกาศแจ้งเตือนที่ชัดเจนแก่ผู้ใช้ และนำทางไปยัง home_default อย่างสะอาดเรียบร้อย
รายการตรวจสอบการตรวจสอบก่อนการใช้งานจริงสำหรับการนำทาง QR แบบสองสถานะ
ก่อนที่จะเผยแพร่สื่อ QR ทางกายภาพ ทีมวิศวกรรมควรดำเนินการตามโปรโตคอลการตรวจสอบเจ็ดประการต่อไปนี้บนอุปกรณ์จริง:
- ขั้นตอนที่ 1: การปลุกแบบ Cold-Start ที่ติดตั้งแล้ว: บังคับปิดแอปที่ติดตั้งไว้และสแกน QR code ยืนยันว่าระบบปฏิบัติการเปิดแอปไปยังหน้าจอเป้าหมายโดยตรง (
product_detail) - ขั้นตอนที่ 2: การปลุกแบบ Warm-Start ที่ติดตั้งแล้วและการสแกนซ้ำ: เปิดแอปทิ้งไว้บนแท็บรอง นำแอปไปไว้เบื้องหลังและสแกน QR code ยืนยันว่า
onNewIntentนำทางไปยังหน้าจอเป้าหมาย นำทางออกไป สแกน QR code เดิมซ้ำเป็นครั้งที่สอง และยืนยันว่ามันนำทางอีกครั้ง แทนที่จะถูกบล็อกโดยการตัดความซ้ำซ้อนที่กว้างเกินไป - ขั้นตอนที่ 3: การกู้คืนแบบดีเฟอร์รีดเมื่อยังไม่ได้ติดตั้ง (เส้นทางสาธารณะ): ถอนการติดตั้งแอป สแกน QR code ดำเนินการติดตั้งจากสโตร์ให้เสร็จสิ้น และเปิดใช้งาน ยืนยันว่าการเรียกกลับพารามิเตอร์แบบดีเฟอร์รีดทำให้เพย์โหลดเป็นมาตรฐานและนำทางไปยัง
product_detail - ขั้นตอนที่ 4: การกู้คืนแบบดีเฟอร์รีดร่วมกับเกตการตรวจสอบสิทธิ์ (เส้นทางที่มีการป้องกัน): ถอนการติดตั้งแอป สแกน QR code ที่มุ่งเป้าไปที่
room_lobbyติดตั้ง และเปิดใช้งานในขณะที่ออกจากระบบอยู่ ยืนยันว่าแอปนำทางไปที่หน้าจอเข้าสู่ระบบก่อน และเมื่อเข้าสู่ระบบเสร็จสิ้น จะบริโภคPendingRouteStoreและนำทางไปยังroom_lobby - ขั้นตอนที่ 5: การปฏิเสธแหล่งกำเนิดที่ไม่น่าเชื่อถือและคีย์ซ้ำ: ทริกเกอร์
ACTION_VIEWintent จากโฮสต์ที่ไม่น่าเชื่อถือหรือมีพารามิเตอร์คิวรีซ้ำ (เช่น?route=product_detail&route=room_lobby) ยืนยันว่าhandleIncomingWakeupIntentปฏิเสธ Intent และบันทึกคำเตือน - ขั้นตอนที่ 6: ความคงทนต่อการหมุนหน้าจอ (Screen Rotation Idempotency): หลังจากมาถึงหน้าจอลิงก์เชิงลึก ให้หมุนอุปกรณ์เพื่อทริกเกอร์การสร้าง Activity ใหม่ ยืนยันว่าแฟล็ก
EXTRA_ROUTE_CONSUMEDป้องกันไม่ให้ Intent ดั้งเดิมทำงานซ้ำ - ขั้นตอนที่ 7: การสำรองข้อมูลแบบออฟไลน์ / ไม่ตรงกันอย่างราบรื่น: เปิดการติดตั้งใหม่ในโหมดเครื่องบิน ยืนยันว่าการหมดเวลาในการดึงข้อมูลได้รับการแก้ไขโดยไม่บล็อกเทรด UI และพาผู้ใช้ไปยัง
home_default
การวิเคราะห์เชิงเปรียบเทียบกลไกการนำทางภายในแอป
การประเมินสกีมา URL, Universal Links และ App Links รวมถึงการกู้คืนฉากแบบดีเฟอร์รีด
แพลตฟอร์มมือถือมีกลไกที่แตกต่างกันสำหรับการนำผู้ใช้เข้าสู่มุมมองแอปพลิเคชันข้ามสถานะการติดตั้งต่างๆ:
| กลไกการนำทาง | สถานะแอปเป้าหมาย | ข้อกำหนดการตรวจสอบสิทธิ์ของ OS | ความคงอยู่ของพารามิเตอร์ข้ามการติดตั้งผ่านสโตร์ | ข้อจำกัดทางวิศวกรรมหลัก |
|---|---|---|---|---|
| Custom URL Scheme | ติดตั้งแล้วเท่านั้น | ไม่มี (ประกาศใน Manifest เท่านั้น) | ไม่ได้ (ล้มเหลวหากไม่ได้ติดตั้งแอป) | ทริกเกอร์ป๊อปอัปข้อผิดพลาดของเบราว์เซอร์หากไม่มีแอป; ช่องโหว่ต่อการถูกไฮแจ็คสกีมา |
| Universal Links / App Links | ติดตั้งแล้วเป็นหลัก | iOS: ไฟล์ AASA + สิทธิ์ Associated Domains; Android: assetlinks.json + ใบรับรองการเซ็นชื่อแอปที่ตรงกันและการกำหนดค่าตัวกรอง Intent ที่ได้รับการตรวจสอบแล้ว |
ไม่ได้ (ต้องมีเลเยอร์ดีเฟอร์รีดสำหรับการติดตั้งใหม่) | อาจถูกระงับโดยคอนเทนเนอร์เว็บวิวในแอปที่ถูกจำกัดบางประเภท |
| Google Play Install Referrer | การติดตั้ง Android ใหม่ | เส้นทางการจัดจำหน่าย Google Play Store อย่างเป็นทางการ | ได้ (สตริงรีเฟอร์เรอร์ที่รองรับ) | เฉพาะ Google Play Store ของ Android; ไม่ครอบคลุม iOS หรือสโตร์อื่นที่ไม่ใช่ Play Store |
| SDK-Assisted Scene Restoration | ติดตั้งแล้ว & ติดตั้งใหม่ | การเชื่อมโยงโดเมน + อะแดปเตอร์ SDK ที่ตรวจสอบแล้ว | ขึ้นอยู่กับกลไกของแพลตฟอร์มที่รองรับ | ต้องมีการตรวจสอบความถูกต้องของ Allowlist เส้นทาง & การตรวจสอบความเป็นส่วนตัวของแพลตฟอร์ม |
ข้อแลกเปลี่ยนทางสถาปัตยกรรมข้ามสถานะการติดตั้งและระบบปฏิบัติการ
Custom URL schemes (เช่น myapp://product/123) ขาดการตรวจสอบความเป็นเจ้าของโดเมนด้วยการเข้ารหัส ซึ่งหมายความว่าแอปพลิเคชันใดๆ สามารถลงทะเบียนสกีมาเดียวกันและดักจับลิงก์ได้ นอกจากนี้ การสแกน QR code ของสกีมาแบบกำหนดเองดิบเมื่อไม่ได้ติดตั้งแอปจะส่งผลให้เกิดข้อผิดพลาดของเบราว์เซอร์ที่ไม่สามารถกู้คืนได้
Universal Links และ Android App Links จะตรวจสอบความเป็นเจ้าของโดเมนผ่าน HTTPS และสำรองข้อมูลไปยังหน้าแลนดิ้งเพจบนเว็บเมื่อไม่มีแอป ซึ่งทำให้สิ่งเหล่านี้กลายเป็นรากฐานที่จำเป็นสำหรับการนำทางด้วย QR การรวม Universal Links และ App Links สำหรับการปลุกแอปที่ติดตั้งแล้วเข้ากับการกู้คืนแบบดีเฟอร์รีดที่รองรับโดยแพลตฟอร์ม (เช่น Google Play Install Referrer บน Android และอะแดปเตอร์ SDK ที่ได้รับการตรวจสอบความเป็นส่วนตัว) ช่วยให้สามารถกู้คืนฉากแบบสองสถานะจากแอสเซท QR เดียวได้
คำถามที่พบบ่อย (FAQ)
พารามิเตอร์ QR แบบกำหนดเองเปิดหน้าเว็บในแอปเฉพาะได้อย่างไร หากยังไม่ได้ติดตั้งแอป
เหตุใดการสแกน QR code ของฉันจึงเปิดแอปไปที่หน้าจอหลักแทนที่จะเป็นหน้าเป้าหมาย
ฉันจะป้องกันไม่ให้ QR code ที่เป็นอันตรายนำผู้ใช้ไปยังหน้าจอภายในที่ไม่ได้รับอนุญาตได้อย่างไร
สรุปและกรอบการตัดสินใจ
ในการสร้าง QR code สำหรับการนำทางในแอปด้วยพารามิเตอร์กำหนดเอง ทีมวิศวกรรมต้องเชื่อมโยงสตริงคิวรี URL ภายนอกแบบกะทัดรัดเข้ากับตัวควบคุมการนำทางเนทีฟที่ตระหนักถึงสถานะ การใช้งานที่สมบูรณ์จะจัดการทั้งการปลุกระบบปฏิบัติการโดยตรงสำหรับผู้ใช้ที่มีอยู่และการกู้คืนพารามิเตอร์แบบดีเฟอร์รีดตามเงื่อนไขสำหรับการติดตั้ง App Store ใหม่
เนื่องจากความต่อเนื่องของพารามิเตอร์ข้าม App Store ขึ้นอยู่กับกลไกเฉพาะของแพลตฟอร์ม เช่น Google Play Install Referrer บน Android และเวิร์กโฟลว์ที่สอดคล้องกับความเป็นส่วนตัวและได้รับการตรวจสอบแล้วบน iOS แอปพลิเคชันจึงต้องปฏิบัติต่อเพย์โหลดการนำทางที่กู้คืนมาว่าเป็นผู้สมัครที่ไม่น่าเชื่อถือ การผสมผสานตัวส่งเส้นทางที่อยู่ใน allowlist, การคิวสถานะการตรวจสอบสิทธิ์ และการสำรองข้อมูลหน้าจอหลักที่ราบรื่น จะช่วยให้ทีมสามารถกู้คืนฉากได้อย่างน่าเชื่อถือโดยไม่ลดทอนความเสถียรของแอปพลิเคชันหรือกฎความเป็นส่วนตัวของแพลตฟอร์ม
SDK และเอกสารสำหรับนักพัฒนาของ OpoInstall มีคอมโพเนนต์สำหรับการนำทางการนำทางแบบเว็บไปสู่วันแอปและการส่งผ่านพารามิเตอร์ ยืนยันลายเซ็น API เฉพาะ สคีมาการเรียกกลับ และพฤติกรรมของแพลตฟอร์มที่รองรับโดยรีลีส SDK ที่คุณใช้งานจริง ก่อนที่จะเปิดตัวในระบบโปรดักชัน
สำหรับรายละเอียดการบูรณาการ โปรดศึกษา เอกสารสำหรับนักพัฒนาของ OpoInstall เข้าถึง ศูนย์ดาวน์โหลด SDK ของ OpoInstall หรือกำหนดค่าโดเมนการนำทางแอปพลิเคชันของคุณใน คอนโซลนักพัฒนาของ OpoInstall
เอกสารที่เกี่ยวข้อง
-
แนวคิด: Scene Restoration, Dual-State Deep Linking, Route Allowlisting, Deferred Parameter Recovery
-
เทคโนโลยี: Universal Links, Android App Links, Google Play Install Referrer, Native Navigation Controllers
-
มาตรฐาน: IETF RFC 3986 Uniform Resource Identifier (URI) Generic Syntax, OWASP Mobile Application Security Verification Standard, OWASP Mobile Top 10 Security Risks and Guidelines
-
อินเทอร์เฟซการบูรณาการ: ตัวตรวจสอบเส้นทางลิงก์เชิงลึกที่เป็นของแอปพลิเคชัน, การเรียกกลับการปลุกและพารามิเตอร์การติดตั้งที่บันทึกไว้สำหรับเวอร์ชัน SDK ที่ใช้งาน, และตัวควบคุมการนำทางแบบเนทีฟของแพลตฟอร์ม
-
เอกสารอย่างเป็นทางการและข้อมูลอ้างอิง:
Share this article



