How to Build a Secure Android Payment SDK from Scratch
This article walks through the complete process of designing and implementing a secure, stable, and easy‑to‑use Android third‑party payment SDK, covering project background, technical challenges, layered architecture, core components, security mechanisms, memory management, UI design, performance optimizations, testing strategies, monitoring, and future roadmap.
Preface
With mobile payments becoming increasingly popular, designing a secure, stable, and user‑friendly third‑party payment SDK is a hot topic for many developers. This article shares the complete process our team followed to design and implement an Android third‑party payment SDK from zero, covering architecture, technology selection, security protection, performance optimization, and other practical experiences.
01 Project Background and Requirements Analysis
Business Scenario
Integrate multiple third‑party payment platforms (Alipay, WeChat Pay, UnionPay, etc.)
Payment page is an H5 page that needs to be displayed inside the app
Support multiple display modes (full‑screen, dialog)
Require comprehensive security protection mechanisms
Require a simple and easy‑to‑use API design
Technical Challenges
Security: How to prevent various security risks?
Compatibility: How to adapt to different Android versions and devices?
Stability: How to ensure the reliability of the callback mechanism?
Usability: How to design a concise API?
Extensibility: How to support future feature extensions?
02 Architecture Design
Overall Architecture
┌─────────────────────────────────────┐
│ Business Layer (App) │
├─────────────────────────────────────┤
│ SDK API Layer │
├─────────────────────────────────────┤
│ UI Layer │ Bridge Layer │
│ Activity │ JavaScript │
│ Dialog │ Interface │
├─────────────────────────────────────┤
│ WebView Core Layer │
├─────────────────────────────────────┤
│ Security Layer │ Config Layer │
│ SecurityConfig │ PaymentConfig │
└─────────────────────────────────────┘Design Flowchart
Core Components
1. PaymentSDK – Core Manager
PaymentSDK is the core manager of the entire payment system and uses a singleton pattern to ensure global uniqueness. This design guarantees consistent global state (such as security configuration and callback management), avoids redundant initialization, and provides a simple unified API entry for developers.
Implementation uses a thread‑safe double‑checked locking pattern, ensuring thread safety while avoiding unnecessary synchronization overhead. Lazy initialization creates the instance only when needed, saving memory and startup time.
class PaymentSDK private constructor() {
companion object {
@Volatile
private var INSTANCE: PaymentSDK? = null
fun getInstance(): PaymentSDK {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: PaymentSDK().also { INSTANCE = it }
}
}
}
fun init(context: Context, debug: Boolean, securityConfig: SecurityConfig)
fun pay(activity: Activity, paymentUrl: String, callback: PaymentCallback)
// ... other methods
}Design Highlights:
Singleton pattern ensures global uniqueness
Lazy initialization saves memory
Thread‑safe double‑checked locking
2. PaymentWebView – Custom WebView
PaymentWebView is the core UI component of the SDK. It is a highly customized WebView container rather than a simple wrapper, solving several key problems:
Security Enhancement: System WebView defaults are too permissive; our custom WebView adds multi‑layer protection, including domain whitelist verification, JavaScript interface control, and file‑access restrictions.
Compatibility Assurance: Different Android versions and manufacturers behave differently; the custom WebView unifies these differences to provide a consistent experience.
Feature Extension: On top of the standard WebView we add payment‑specific features such as automatic AuthToken injection, third‑party app launching, and payment result callbacks.
Resource Management: Automatic resource cleanup prevents memory leaks, which is crucial for long‑running apps.
class PaymentWebView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : WebView(context, attrs, defStyleAttr) {
fun setupWebView(config: WebViewConfig?, listener: PaymentWebViewListener?)
fun loadPaymentUrl(url: String)
fun cleanup()
}Design Highlights:
Encapsulates all complex WebView logic
Unified security configuration and error handling
Automatic resource cleanup
3. PaymentJsBridge – JavaScript Bridge
PaymentJsBridge connects the H5 page with native Android code, directly affecting the security and stability of the payment flow. It handles multiple interaction scenarios:
Multi‑format Data Handling: H5 may send complex JSON results or simple status strings; the bridge intelligently parses different formats.
Security Protection: Implements multi‑layer defenses: rate limiting, input length checks, JSON depth validation, and sensitive information filtering.
Asynchronous Processing: JavaScript calls are asynchronous, but UI operations must run on the main thread; a thread‑safe mechanism ensures correct thread usage.
Dynamic Feature Injection: AuthToken injection demonstrates flexibility to add data/functions dynamically based on business needs.
Method Refactoring: Large methods are split into small, single‑responsibility methods to improve readability, testability, and maintainability.
class PaymentJsBridge(
listener: PaymentJsBridgeListener,
securityConfig: SecurityConfig? = null
) {
@JavascriptInterface
fun onWebPayResult(resultJson: String)
@JavascriptInterface
fun onPageLoaded()
@JavascriptInterface
fun getAuthToken(): String
// Security verification
private fun isValidCall(): Boolean
private fun validateJsonDepth(jsonObject: JSONObject): Boolean
}Design Highlights:
Multi‑layer security verification
Rate‑limit to prevent malicious calls
JSON depth check to avoid recursive attacks
Dynamic AuthToken injection support
Method refactoring improves maintainability
03 Security Protection Practices
1. Input Validation and Filtering
Input validation is the first and most important line of defense. In the payment SDK, inputs mainly come from JavaScript calls on the H5 page, which may contain malicious data or exceed expected limits. Our strategy uses a "whitelist + blacklist" dual mechanism:
Length Limitation: Prevents memory‑overflow attacks by setting reasonable upper bounds.
Structure Validation: JSON depth checks prevent stack overflow caused by recursive parsing.
Sensitive Information Filtering: In production, logs automatically mask passwords, tokens, etc.; this can be disabled in debug mode for easier troubleshooting.
Format Verification: Ensures input conforms to expected formats, preventing format‑string and injection attacks.
// JSON length limit
if (resultJson.length > config.maxJsonLength) {
handleSecurityError("Payment result data too long")
return
}
// JSON depth validation
if (!validateJsonDepth(jsonObject)) {
handleSecurityError("Data structure too complex")
return
}
private fun sanitizeMessage(message: String): String {
return if (isDebug) {
message
} else {
sensitivePatterns.fold(message) { acc, pattern ->
acc.replace(pattern, "***")
}
}
}2. Domain Whitelist Mechanism
The domain whitelist prevents malicious web pages from hijacking the WebView. It supports multiple matching modes:
Exact match for known fixed domains
Wildcard match for multiple sub‑domains of the same provider
Sub‑domain match for large site hierarchies
The whitelist can be updated at runtime without releasing a new version, allowing rapid response to new threats.
object DomainUtils {
fun isUrlInWhitelist(url: String?, whitelist: List<String>?): Boolean {
if (whitelist.isNullOrEmpty()) return true // No restriction
val domain = extractDomain(url) ?: return false
return whitelist.any { whitelistDomain ->
isDomainMatched(domain, whitelistDomain.lowercase())
}
}
private fun isDomainMatched(domain: String, pattern: String): Boolean {
return when {
domain == pattern -> true // Exact match
pattern.contains("*") -> Pattern.matches(
pattern.replace(".", "\\.").replace("*", ".*"),
domain
) // Wildcard
pattern.startsWith(".") -> domain.endsWith(pattern) // Sub‑domain
else -> false
}
}
}3. Rate‑Limit Protection
Rate limiting defends against DoS attacks and malicious rapid calls:
Short‑term rapid‑call limit: Detects bursts of calls in a short window.
Long‑term call‑rate control: Monitors calls over a longer period to block sustained low‑frequency attacks.
Dynamic threshold adjustment: Adapts limits based on device performance and network conditions.
Graceful degradation: Logs the incident, notifies monitoring, and provides user‑friendly error messages instead of outright denial.
Reset mechanism: Allows counters to be reset to avoid false positives for normal users.
private fun isValidCall(): Boolean {
val currentTime = System.currentTimeMillis()
// Check rapid interval
if (currentTime - lastCallTime < config.minCallInterval) {
rapidCallCount++
if (rapidCallCount > config.maxRapidCalls) {
return false
}
} else {
rapidCallCount = 0
}
// Check per‑minute limit
if (currentTime - windowStartTime > RATE_LIMIT_WINDOW) {
windowStartTime = currentTime
callCount = 0
}
callCount++
return callCount <= config.maxCallsPerMinute
}04 Memory Management and Lifecycle
Callback Manager Design
In Activity mode, the SDK must guarantee callback reliability even when the Activity is reclaimed by the system. We elevated the callback manager to the Application level using a singleton, ensuring it is never reclaimed. Strong references prevent GC, and duplicate‑call protection avoids processing the same result multiple times.
object PaymentCallbackManager {
private var currentCallback: PaymentCallback? = null
private var isCallbackProcessed = false
fun setCallback(callback: PaymentCallback) {
clearCallback()
currentCallback = callback
isCallbackProcessed = false
}
fun handlePaymentResult(result: PaymentResult) {
if (isCallbackProcessed) {
LogUtils.w(TAG, "Callback already processed, ignore duplicate")
return
}
val callback = currentCallback ?: run {
LogUtils.w(TAG, "Callback not set, cannot handle result")
return
}
isCallbackProcessed = true
try {
when {
PaymentStatus.isSuccess(result.status) -> callback.onPaymentSuccess(result)
PaymentStatus.isFailed(result.status) -> callback.onPaymentFailed(result)
PaymentStatus.isCancelled(result.status) -> callback.onPaymentCancelled(result)
}
} catch (e: Exception) {
LogUtils.e(TAG, "Exception while handling payment result", e)
callback.onPaymentError(e)
}
}
}Key Points
Strong reference guarantees callback is not GC‑ed
Removed complex timeout logic, simplifying the flow
Duplicate‑call protection improves stability
Exception handling ensures safe callback execution
Resource Cleanup Mechanism
Proper cleanup prevents memory leaks, especially for heavyweight components like WebView. Our strategy includes:
Removing all registered JavaScript interfaces
Clearing history, cache, and loading a blank page
Nullifying references to help the GC
Wrapping the entire process in try‑catch to guarantee continuation even if a step fails
Executing cleanup in Activity's onDestroy lifecycle method
class PaymentWebView {
fun cleanup() {
try {
paymentJsBridge?.cleanup()
paymentJsBridge = null
clearHistory()
clearCache(true)
loadUrl("about:blank")
removeJavascriptInterface("WeliPayment")
} catch (e: Exception) {
LogUtils.e(TAG, "Exception during resource cleanup", e)
}
}
}05 UI Design and User Experience
Dual‑Mode Support
We provide both Activity (full‑screen) and Dialog (popup) display modes:
Activity mode: Immersive full‑screen experience, suitable for complex payment flows, benefits from system‑level lifecycle management.
Dialog mode: Popup presentation, customizable size and position, does not affect the underlying page state.
The core logic, security, and callback handling remain identical; only the presentation differs, achieving "one code base, multiple experiences".
enum class LoadMode { ACTIVITY, DIALOG }
// Usage example
PaymentSDK.getInstance().pay(
activity = this,
paymentUrl = url,
loadMode = LoadMode.DIALOG,
paymentConfig = PaymentConfig.createBottomDialog()
)Configurable UI Parameters
Our UI configuration follows the "convention over configuration" principle: sensible defaults work for most scenarios, while developers can fine‑tune parameters when needed.
Size control: Uses ratios instead of absolute pixels to ensure consistent appearance across screen densities.
Position control: Supports various gravity settings to place the payment window anywhere on the screen.
Visual effects: Corner radius, background transparency, etc., allow seamless integration with the host app's style.
Interaction behavior: Configurable cancelability and outside‑click handling.
data class PaymentConfig(
val widthRatio: Float = 1.0f,
val heightRatio: Float = 0.67f,
val gravity: Int = Gravity.BOTTOM,
val cornerRadius: Float = 16f,
val backgroundAlpha: Float = 0.6f,
val cancelable: Boolean = true,
val webViewConfig: WebViewConfig? = null
)06 Performance Optimization Practices
1. WebView Optimization
WebView performance is critical for payment flow smoothness. Our optimizations include:
Rendering priority: Set high priority to ensure fast page rendering.
Cache strategy: Use LOAD_NO_CACHE to always fetch the latest payment data, avoiding stale information.
Security configuration: Dynamically adjust WebSettings based on security mode (strict SSL for production, compatibility mode for testing).
private fun configureWebViewSettings() {
with(settings) {
javaScriptEnabled = config.javaScriptEnabled
domStorageEnabled = config.domStorageEnabled
cacheMode = WebSettings.LOAD_NO_CACHE
setRenderPriority(WebSettings.RenderPriority.HIGH)
allowFileAccess = securityConfig.allowFileAccess
allowContentAccess = securityConfig.allowContentAccess
mixedContentMode = if (securityConfig.enableStrictSSL) {
WebSettings.MIXED_CONTENT_NEVER_ALLOW
} else {
WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE
}
}
}2. Memory Optimization
Key memory‑saving tactics:
Immediate cleanup of resources when components are destroyed.
Use WeakReference for long‑lived objects, but retain strong references where correctness is critical.
Prefer ApplicationContext over ActivityContext to avoid leaks.
All cleanup steps are wrapped in try‑catch to guarantee execution.
override fun onDestroy() {
super.onDestroy()
try {
paymentWebView.cleanup()
paymentWebView.destroy()
} catch (e: Exception) {
LogUtils.e(TAG, "WebView destroy exception", e)
}
}
fun init(context: Context) {
applicationContext = WeakReference(context.applicationContext)
}3. Log Optimization
Logging is essential for debugging and production monitoring, but must be efficient and privacy‑aware:
In production, automatically mask passwords, tokens, and possible card numbers.
Avoid unnecessary string formatting when the log level is disabled.
Different log levels have distinct output policies; debug logs only appear in development.
Minimize temporary object creation to reduce GC pressure.
object LogUtils {
private val sensitivePatterns = listOf(
Regex("password[\"']?\\s*[:=]\\s*[\"']?[^\\s,}]+", RegexOption.IGNORE_CASE),
Regex("token[\"']?\\s*[:=]\\s*[\"']?[^\\s,}]+", RegexOption.IGNORE_CASE),
Regex("\\b\\d{13,19}\\b") // possible card number
)
private fun sanitizeMessage(message: String): String {
return if (isDebug) {
message
} else {
sensitivePatterns.fold(message) { acc, pattern ->
acc.replace(pattern, "***")
}
}
}
}07 Testing Strategy
1. Unit Tests
Focus on core business logic, edge cases, and security checks. Use mock objects to isolate external dependencies and verify both normal and exceptional paths.
@Test
fun testDomainWhitelist() {
val whitelist = listOf("*.example.com", "test.com")
assertTrue(DomainUtils.isUrlInWhitelist("https://pay.example.com", whitelist))
assertTrue(DomainUtils.isUrlInWhitelist("https://test.com", whitelist))
assertFalse(DomainUtils.isUrlInWhitelist("https://malicious.com", whitelist))
}
@Test
fun testPaymentResultParsing() {
val jsonString = """{"status":"PAYED","message":"Payment succeeded"}"""
val bridge = PaymentJsBridge(mockListener)
bridge.onPaymentResult(jsonString)
verify(mockListener).onPaymentResult(any())
}2. Integration Tests
Create full‑stack test pages that simulate real payment scenarios, covering various data formats, statuses, and error conditions. These pages are also used for performance and UX testing.
<!-- test_activity_callback.html -->
<!DOCTYPE html>
<html>
<head><title>Activity Callback Test</title></head>
<body>
<button onclick="testSuccess()">Test Success</button>
<button onclick="testFailed()">Test Failure</button>
<button onclick="testCancel()">Test Cancel</button>
<script>
function testSuccess() {
WeliPayment.onPaymentResult(JSON.stringify({
status: "PAYED",
message: "Payment succeeded",
responseData: "{\"orderId\":\"123456\"}"
}));
}
// ... other test methods
</script>
</body>
</html>3. Stress Tests
Simulate high‑frequency calls, memory‑constrained environments, concurrent payment flows, and long‑running usage to verify stability, thread safety, and resource handling.
@Test
fun testRapidCalls() {
val bridge = PaymentJsBridge(mockListener, securityConfig = SecurityConfig.createStrict())
repeat(10) {
bridge.onPaymentResult("""{"status":"PAYED"}""")
}
verify(mockListener, times(3)).onPaymentResult(any())
}08 Online Monitoring and Data Analysis
Key Metrics
Success Rate: Payment initiation success, H5 page load success, callback delivery success.
Performance: Page load time, JavaScript execution time, memory usage.
Security: Number of malicious calls blocked, whitelist hit rate, exception statistics.
Instrumentation
We embed tracking points at every critical step (payment start, page load, result callback) while ensuring user privacy by not collecting sensitive data and applying de‑identification.
object PaymentAnalytics {
fun trackPaymentStart(url: String, mode: LoadMode) {
Analytics.track("payment_start", mapOf(
"url_domain" to extractDomain(url),
"load_mode" to mode.name,
"timestamp" to System.currentTimeMillis()
))
}
fun trackPaymentResult(result: PaymentResult, duration: Long) {
Analytics.track("payment_result", mapOf(
"status" to result.status,
"duration_ms" to duration,
"error_code" to result.errorCode
))
}
}09 Continuous Optimization and Version Iteration
Version Roadmap
v1.0: Core functionality, Activity/Dialog support
v1.1: Enhanced security, domain whitelist
v1.2: Performance improvements, memory management
v1.3: UX refinements, error handling
Technical Debt Management
Regular code reviews after each feature.
Scheduled refactoring of high‑complexity modules.
Continuous documentation updates.
Maintain >80% test coverage.
10 Experience Summary and Best Practices
Success Lessons
Security first: design security from the beginning.
Progressive enhancement: implement core features first, then add advanced capabilities.
User‑centric API: keep the interface simple to lower integration cost.
Comprehensive monitoring: online metrics help quickly locate and fix issues.
Pitfalls Encountered
Overusing WeakReference caused callbacks to become invalid.
Improper lifecycle handling led to lost callbacks when Activity was reclaimed.
WebView compatibility varied across Android versions.
Insufficient JavaScript bridge security opened attack vectors.
Thread‑unsafe WebView calls caused runtime exceptions.
Large monolithic methods were hard to maintain; refactoring into small units was necessary.
Complex timeout logic reduced system stability; simplifying it improved reliability.
Future Plans
Support Kotlin coroutines for asynchronous operations.
Explore Jetpack Compose integration.
Add multi‑process support for isolated WebView execution.
Adopt plugin architecture for dynamic payment module loading.
Integrate AI to automatically detect and handle anomalies.
Enhance performance monitoring and auto‑optimization.
Internationalization: multi‑language and multi‑region payment scenarios.
Conclusion
Building an excellent payment SDK requires balancing security, stability, performance, and usability. Through continuous practice and optimization, our SDK now reliably serves production environments.
We hope this article provides useful references and inspiration for developers working on similar solutions. Feel free to discuss any questions or suggestions!
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
WeiLi Technology Team
Practicing data-driven principles and believing technology can change the world.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
