Makuhari Development Corporation
6 min read, 1096 words, last updated: 2025/12/10
TwitterLinkedInFacebookEmail

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

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

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
}

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()
        }
    }
}

When implementing deep links for financial transactions, especially crypto wallet payments, security becomes paramount.

Critical Security Risks

  1. Parameter Tampering: Attackers modify payment amounts or recipient addresses
  2. Man-in-the-Middle Attacks: Interception and modification of deep link parameters
  3. App Impersonation: Malicious apps claiming the same URL scheme
  4. XSS Vulnerabilities: Web page compromise leading to parameter manipulation

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.
Makuhari Development Corporation
法人番号: 6040001134259
サイトマップ
ご利用にあたって
個人情報保護方針
個人情報取扱に関する同意事項
お問い合わせ
Copyright© Makuhari Development Corporation. All Rights Reserved.