Complete Guide to H5-to-App Deep Linking with Security Best Practices
Opening mobile apps from web pages (H5) is a common requirement in modern app development. Whether you're building an e-commerce platform, a crypto payment system, or any app that needs seamless web-to-app transitions, understanding deep linking is crucial. This comprehensive guide covers implementation methods across iOS and Android platforms, with special attention to security considerations for payment scenarios.
Prerequisites
Before diving into implementation, ensure you have:
- Basic understanding of mobile app development (iOS/Android)
- Knowledge of web development (JavaScript/HTML5)
- Understanding of URL schemes and web protocols
- For crypto payment scenarios: familiarity with blockchain wallet security
Understanding Deep Link Implementation Methods
1. Custom URL Schemes (Universal Support)
The most straightforward approach works across both iOS and Android:
// H5 JavaScript implementation
function openApp() {
window.location.href = "myapp://action/openOrder?orderId=123&from=h5";
}
// Fallback mechanism
function openAppWithFallback() {
window.location.href = "myapp://action/openOrder?orderId=123";
// Redirect to download page if app not installed
setTimeout(() => {
window.location.href = "https://example.com/download";
}, 1500);
}Characteristics:
- Pros: Works on both platforms, no user permissions required
- Cons: No graceful fallback if app isn't installed
2. iOS Universal Links
Universal Links provide a more sophisticated approach for iOS:
<!-- H5 HTML -->
<a href="https://example.com/app/open?scene=promotion&id=999">
Open in App
</a>Implementation requirements:
- Configure
apple-app-site-association(AASA) file - Set up associated domains in iOS app
- Implement URL handling in app delegate
// iOS Swift implementation
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
if let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
let queryItems = components.queryItems {
// Parse parameters and execute app functionality
let scene = queryItems.first(where: { $0.name == "scene" })?.value
let id = queryItems.first(where: { $0.name == "id" })?.value
// Route to appropriate functionality
handleDeepLink(scene: scene, id: id)
}
return true
}3. Android App Links and Intent Schemes
Android offers multiple approaches:
App Links:
<!-- Android Manifest -->
<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"/>
<data android:scheme="https"
android:host="example.com"
android:pathPrefix="/app/open"/>
</intent-filter>Intent Scheme (with fallback):
// H5 JavaScript for Android
function openAndroidApp() {
window.location.href = "intent://open?scene=pay#Intent;scheme=myapp;package=com.xxx.myapp;S.browser_fallback_url=https%3A%2F%2Fexample.com%2Fdownload;end";
}Android Activity handling:
// Kotlin implementation
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val data = intent.data
if (data != null) {
val scene = data.getQueryParameter("scene")
val amount = data.getQueryParameter("amount")
// Execute specific app functionality
when (scene) {
"pay" -> startPaymentFlow(amount)
"scan" -> openScanner()
else -> navigateToHome()
}
}
}Deep Link Security for Payment Scenarios
When implementing deep links for financial transactions, especially crypto wallet payments, security becomes paramount.
Critical Security Risks
- Parameter Tampering: Attackers modify payment amounts or recipient addresses
- Man-in-the-Middle Attacks: Interception and modification of deep link parameters
- App Impersonation: Malicious apps claiming the same URL scheme
- XSS Vulnerabilities: Web page compromise leading to parameter manipulation
Implementing Secure Payment Deep Links
Server-Side Parameter Signing
Never trust client-side parameters for payment information:
// H5 - Request signed payment parameters from server
async function initiateSecurePayment(orderId) {
try {
const response = await fetch('/api/payment/prepare', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ orderId })
});
const { signedPayload } = await response.json();
// Use signed payload in deep link
window.location.href = `cryptowallet://pay?payload=${signedPayload}`;
} catch (error) {
console.error('Payment preparation failed:', error);
}
}Server-side payload structure:
{
"to": "0xabc123...",
"amount": "1.2",
"currency": "ETH",
"nonce": "38792",
"expire": 1733820000,
"orderId": "order_123",
"signature": "server_hmac_signature"
}Wallet App Security Implementation
Crypto wallets must implement rigorous validation:
// Android Kotlin - Secure payment parameter handling
data class PaymentRequest(
val to: String,
val amount: String,
val currency: String,
val nonce: String,
val expire: Long,
val orderId: String,
val signature: String
)
class SecurePaymentHandler {
fun processPaymentDeepLink(payload: String): Boolean {
try {
val paymentRequest = parseAndValidatePayload(payload)
// Critical security checks
if (!verifyServerSignature(paymentRequest)) {
throw SecurityException("Invalid signature")
}
if (System.currentTimeMillis() > paymentRequest.expire) {
throw SecurityException("Expired payment request")
}
if (hasNonceBeenUsed(paymentRequest.nonce)) {
throw SecurityException("Duplicate nonce")
}
// Show user confirmation dialog
showPaymentConfirmation(paymentRequest)
return true
} catch (e: Exception) {
logSecurityViolation(e)
return false
}
}
private fun verifyServerSignature(request: PaymentRequest): Boolean {
val payload = "${request.to}:${request.amount}:${request.nonce}:${request.expire}"
val expectedSignature = hmacSha256(payload, serverPublicKey)
return expectedSignature == request.signature
}
}Platform-Specific Security Considerations
iOS Security Measures
// iOS - Secure Universal Link handling
func handleUniversalLink(_ url: URL) -> Bool {
// Verify the link comes from trusted domain
guard url.host == "yourtrusted.domain.com" else {
logSecurityViolation("Untrusted domain: \(url.host ?? "unknown")")
return false
}
// Extract and validate signed payload
if let payload = url.queryParameters["payload"] {
return processSecurePayment(payload)
}
return false
}
extension URL {
var queryParameters: [String: String] {
var parameters: [String: String] = [:]
URLComponents(url: self, resolvingAgainstBaseURL: true)?
.queryItems?
.forEach { parameters[$0.name] = $0.value }
return parameters
}
}Android Security Best Practices
<!-- Manifest - Secure App Link declaration -->
<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" />
<!-- Use specific host and path to prevent hijacking -->
<data android:scheme="https"
android:host="secure.yourapp.com"
android:pathPrefix="/payment/secure" />
</intent-filter>Complete Implementation Example
Here's a production-ready implementation combining security and functionality:
// H5 - Complete deep link implementation with security
class SecureDeepLinkManager {
constructor(config) {
this.serverEndpoint = config.serverEndpoint;
this.appScheme = config.appScheme;
this.fallbackUrl = config.fallbackUrl;
}
async openApp(action, params = {}) {
try {
// Get signed parameters from server
const signedData = await this.getSignedParameters(action, params);
// Try multiple deep link methods
const success = await this.attemptDeepLink(signedData);
if (!success) {
// Fallback to download page
window.location.href = this.fallbackUrl;
}
} catch (error) {
console.error('Deep link failed:', error);
this.handleError(error);
}
}
async getSignedParameters(action, params) {
const response = await fetch(`${this.serverEndpoint}/deeplink/sign`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action, params })
});
if (!response.ok) {
throw new Error('Failed to get signed parameters');
}
return await response.json();
}
async attemptDeepLink(signedData) {
const payload = encodeURIComponent(JSON.stringify(signedData));
// iOS - Try Universal Link first
if (this.isIOS()) {
window.location.href = `https://yourapp.com/deeplink?payload=${payload}`;
return await this.waitForAppOpen();
}
// Android - Try App Link, then Intent, then URL Scheme
if (this.isAndroid()) {
// Try App Link
window.location.href = `https://yourapp.com/deeplink?payload=${payload}`;
if (!await this.waitForAppOpen()) {
// Try Intent scheme
window.location.href = `intent://deeplink?payload=${payload}#Intent;scheme=${this.appScheme};package=com.yourapp.package;end`;
}
return await this.waitForAppOpen();
}
// Fallback to URL Scheme
window.location.href = `${this.appScheme}://deeplink?payload=${payload}`;
return await this.waitForAppOpen();
}
waitForAppOpen(timeout = 2000) {
return new Promise((resolve) => {
let startTime = Date.now();
let hidden = false;
const handleVisibilityChange = () => {
if (document.hidden) {
hidden = true;
resolve(true);
}
};
const handlePageShow = () => {
if (Date.now() - startTime > timeout && !hidden) {
resolve(false);
}
};
document.