วิธีสร้าง QR Code สำหรับการนำทางในแอปพร้อมพารามิเตอร์กำหนดเอง

opoinstall
2026-10-29
5 min read

จะเชื่อมโยงพารามิเตอร์กำหนดเองเมื่อสร้าง 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 แบบกำหนดพารามิเตอร์สามารถกู้คืนหน้าจอรายละเอียดสินค้าภายในแอปได้

ปัญหาการละทิ้งที่หน้าจอหลัก: เหตุใดการเปลี่ยนเส้นทางไปยังสโตร์ทั่วไปจึงขัดขวางเส้นทางผู้ใช้ที่มีความตั้งใจสูง

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 ภายนอกเด็ดขาด

พารามิเตอร์ URL ของ QR ถูกแปลงให้อยู่ในรูปแบบมาตรฐาน (normalized) เป็นบริบทการนำทางและการระบุแหล่งที่มาที่แยกจากกัน

ในโมเดลสองชั้นนี้:

  • แท็กการระบุแหล่งที่มาแบบ passive (channelCode →\to channel_code) จะถูกใช้โดยไปพไลน์การวิเคราะห์เพื่อวัดปริมาณการแปลงทางการตลาด (Conversion volume) โดยไม่ต้องเปลี่ยนสถานะ UI ของไคลเอนต์
  • พารามิเตอร์การนำทางที่ใช้งานได้ (route →\to route_key และ id →\to target_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:

  1. การตรวจสอบแหล่งกำเนิด (Direct Intents): ตรวจสอบว่า URI ของ Intent.ACTION_VIEW ขาเข้าใช้สกีมา https ตรงกับโฮสต์ที่เกี่ยวข้องที่ได้รับการตรวจสอบแล้วของแอปพลิเคชัน (promo.example.com) และพาธ (/open) และไม่มีคิวรีคีย์ซ้ำที่ขัดแย้งกัน
  2. การ Normalize สคีมาและการตรวจสอบ Allowlist: แมปคีย์ route / id ภายนอกหรือฟิลด์ route_key / target_id ภายในเข้ากับโมเดลทางการ โดยปฏิเสธเส้นทางที่ไม่อยู่ใน allowlist หรือ ID ที่จัดรูปแบบไม่ถูกต้อง โปรดทราบว่าตรรกะการแยกวิเคราะห์ Kotlin ของไคลเอนต์ทำหน้าที่เป็นโครงร่างการแยกวิเคราะห์เชิงป้องกันที่เป็นตัวอย่าง ในระบบโปรดักชัน ให้แยกวิเคราะห์เพย์โหลดเทียบกับกฎสคีมาทางการทั้งหมดก่อนทำการส่งต่อ
  3. ตัวป้องกันการเข้ามาซ้ำแบบ Single-Flight: ใช้ตัวป้องกันแบบอะตอมิกต่อการส่งมอบ เพื่อไม่ให้การเรียกกลับแบบพร้อมกันทริกเกอร์ธุรกรรมแฟรกเมนต์ที่ทับซ้อนกัน

การจัดการเกตการตรวจสอบสิทธิ์: การคิวเส้นทางลิงก์เชิงลึกที่รอดำเนินการจนกว่าการเข้าสู่ระบบของผู้ใช้จะเสร็จสมบูรณ์

เส้นทาง QR ที่ได้รับการป้องกันสามารถดำเนินต่อได้หลังจากการตรวจสอบสิทธิ์ ในขณะที่เป้าหมายที่ไม่ถูกต้องจะสำรองกลับอย่างปลอดภัย

เมื่อผู้ใช้ใหม่ติดตั้งแอปพลิเคชันจาก 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_VIEW intent จากโฮสต์ที่ไม่น่าเชื่อถือหรือมีพารามิเตอร์คิวรีซ้ำ (เช่น ?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 จะเปิดหน้าแลนดิ้งเพจบนเว็บที่บันทึกพารามิเตอร์การนำทางก่อนที่จะนำผู้ใช้ไปยัง App Store เมื่อเปิดใช้งานครั้งแรกหลังจากการติดตั้ง แอปพลิเคชันจะพยายามกู้คืนพารามิเตอร์ผ่านกลไกการกู้คืนแบบดีเฟอร์รีดที่รองรับโดยแพลตฟอร์ม ตรวจสอบความถูกต้องของเส้นทางเป้าหมาย และนำทางไปยังหน้าจอที่กำหนด
เหตุใดการสแกน QR code ของฉันจึงเปิดแอปไปที่หน้าจอหลักแทนที่จะเป็นหน้าเป้าหมาย
การตกลงมาที่หน้าจอหลักมักเกิดขึ้นจากสามสาเหตุ: Activity ของ Android ถูกกำหนดค่าเป็น `singleTop` หรือ `singleTask` โดยไม่มีการโอเวอร์라이드 `onNewIntent` เพื่อจับ Intent ของการเริ่มต้นแบบอุ่น (Warm-start), เราเตอร์การนำทางทำงานก่อนที่กราฟ UI หลักจะขยายตัวเสร็จสมบูรณ์ หรือหน้าจอเป้าหมายต้องการการตรวจสอบสิทธิ์ผู้ใช้และละทิ้งเส้นทางที่รอดำเนินการระหว่างขั้นตอนการเข้าสู่ระบบ
ฉันจะป้องกันไม่ให้ QR code ที่เป็นอันตรายนำผู้ใช้ไปยังหน้าจอภายในที่ไม่ได้รับอนุญาตได้อย่างไร
ห้ามส่งพาธ URL ดิบไปยังตัวควบคุมการนำทางของคุณโดยตรง ตรวจสอบสกีมา URI, โฮสต์ และพาธขาเข้า บังคับใช้ allowlist ฝั่งไคลเอนต์ที่เข้มงวดของคีย์เส้นทางที่อนุญาต ตรวจสอบความถูกต้องของรูปแบบ ID ของเอนทิตีโดยใช้ Regular Expressions และตรวจสอบทั้งสถานะการตรวจสอบสิทธิ์ของไคลเอนต์และสิทธิ์ทรัพยากรฝั่งเซิร์ฟเวอร์ก่อนที่จะแสดงหน้าจอที่ได้รับการป้องกัน

สรุปและกรอบการตัดสินใจ

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

เนื่องจากความต่อเนื่องของพารามิเตอร์ข้าม App Store ขึ้นอยู่กับกลไกเฉพาะของแพลตฟอร์ม เช่น Google Play Install Referrer บน Android และเวิร์กโฟลว์ที่สอดคล้องกับความเป็นส่วนตัวและได้รับการตรวจสอบแล้วบน iOS แอปพลิเคชันจึงต้องปฏิบัติต่อเพย์โหลดการนำทางที่กู้คืนมาว่าเป็นผู้สมัครที่ไม่น่าเชื่อถือ การผสมผสานตัวส่งเส้นทางที่อยู่ใน allowlist, การคิวสถานะการตรวจสอบสิทธิ์ และการสำรองข้อมูลหน้าจอหลักที่ราบรื่น จะช่วยให้ทีมสามารถกู้คืนฉากได้อย่างน่าเชื่อถือโดยไม่ลดทอนความเสถียรของแอปพลิเคชันหรือกฎความเป็นส่วนตัวของแพลตฟอร์ม

SDK และเอกสารสำหรับนักพัฒนาของ OpoInstall มีคอมโพเนนต์สำหรับการนำทางการนำทางแบบเว็บไปสู่วันแอปและการส่งผ่านพารามิเตอร์ ยืนยันลายเซ็น API เฉพาะ สคีมาการเรียกกลับ และพฤติกรรมของแพลตฟอร์มที่รองรับโดยรีลีส SDK ที่คุณใช้งานจริง ก่อนที่จะเปิดตัวในระบบโปรดักชัน

สำหรับรายละเอียดการบูรณาการ โปรดศึกษา เอกสารสำหรับนักพัฒนาของ OpoInstall เข้าถึง ศูนย์ดาวน์โหลด SDK ของ OpoInstall หรือกำหนดค่าโดเมนการนำทางแอปพลิเคชันของคุณใน คอนโซลนักพัฒนาของ OpoInstall

เอกสารที่เกี่ยวข้อง

Share this article

Keep Discovering

GitHub เปิดตัว Copilot Fusion? ทำไมโค้ดแบบหลายโมเดลจึงมีความสำคัญ

GitHub เปิดตัว Copilot Fusion? ทำไมโค้ดแบบหลายโมเดลจึงมีความสำคัญ

GitHub เปิดตัวรันไทม์ตัวจัดระเบียบโมเดล Copilot Fusion ค้นพบวิธีที่การกำหนดเส้นทางโมเดลแบบไดนามิกและรูปแบบการทำงานช่วยลดต้นทุนโทเค็นและความหน่วงในการเขียนโค้ด

วิธีป้องกันการเปลี่ยนเส้นทางแบบออฟไลน์ด้วยการรักษาความปลอดภัยคัสตอม QR Code

วิธีป้องกันการเปลี่ยนเส้นทางแบบออฟไลน์ด้วยการรักษาความปลอดภัยคัสตอม QR Code

เรียนรู้วิธีรักษาความปลอดภัยคัสตอม QR Code จากการถูกไฮแจ็คการสแกนด้วย HTTPS, ลายเซ็น HMAC, การตรวจสอบการงัดแงะทางกายภาพ และการตรวจจับความเสี่ยง

Google เปิดตัว Connected Apps? แอปพลิเคชันลิงก์จะเปลี่ยนไปอย่างไร

Google เปิดตัว Connected Apps? แอปพลิเคชันลิงก์จะเปลี่ยนไปอย่างไร

Google เปิดตัวแพลตฟอร์ม Connected Apps สำหรับ Gemini และ Search ค้นหาว่าโปรโตคอล MCP และ AppFunctions กำลังปรับเปลี่ยนการเชื่อมโยงแอป การเส้นทาง และการระบุแหล่งที่มาอย่างไร