11package com.braintreepayments.api
22
33import android.annotation.SuppressLint
4- import android.content.ActivityNotFoundException
54import android.content.Intent
65import android.net.Uri
7- import android.os.Handler
8- import android.os.Looper
96import android.webkit.WebView
107import androidx.activity.ComponentActivity
118import androidx.core.net.toUri
@@ -15,10 +12,10 @@ import com.braintreepayments.api.PopupBridgeAnalytics.POPUP_BRIDGE_FAILED
1512import com.braintreepayments.api.PopupBridgeAnalytics.POPUP_BRIDGE_SUCCEEDED
1613import com.braintreepayments.api.internal.AnalyticsClient
1714import com.braintreepayments.api.internal.AnalyticsParamRepository
15+ import com.braintreepayments.api.internal.AppSwitchHandler
1816import com.braintreepayments.api.internal.PendingRequestRepository
1917import com.braintreepayments.api.internal.PopupBridgeJavascriptInterface
2018import com.braintreepayments.api.internal.PopupBridgeJavascriptInterface.Companion.POPUP_BRIDGE_URL_HOST
21- import com.braintreepayments.api.internal.PAYPAL_APP_PACKAGE
2219import com.braintreepayments.api.internal.isPayPalInstalled
2320import java.lang.ref.WeakReference
2421import kotlinx.coroutines.CoroutineScope
@@ -32,7 +29,7 @@ class PopupBridgeClient @SuppressLint("SetJavaScriptEnabled") internal construct
3229 private val returnUrlScheme : String ,
3330 private val popupBridgeWebViewClient : PopupBridgeWebViewClient ,
3431 private val browserSwitchClient : BrowserSwitchClient ,
35- private val enablePopupBridgeAppSwitch : Boolean = false ,
32+ private val enablePopupBridgeAppSwitch : Boolean = false ,
3633 private val pendingRequestRepository : PendingRequestRepository =
3734 PendingRequestRepository (activity.applicationContext),
3835 private val coroutineScope : CoroutineScope = activity.lifecycleScope,
@@ -49,7 +46,6 @@ private val enablePopupBridgeAppSwitch: Boolean = false,
4946) {
5047 private val activityRef = WeakReference (activity)
5148 private val webViewRef = WeakReference (webView)
52- private val mainHandler = Handler (Looper .getMainLooper())
5349
5450 private var navigationListener: PopupBridgeNavigationListener ? = null
5551 private var messageListener: PopupBridgeMessageListener ? = null
@@ -58,19 +54,18 @@ private val enablePopupBridgeAppSwitch: Boolean = false,
5854 /* *
5955 * Browser-switch path only: ensures [handleReturnToApp] completes the pending browser request at most once
6056 * when both `onResume` and `onNewIntent` invoke it (singleTop / singleTask / singleInstance).
61- *
62- * Not used for native app-switch returns; see [expectingAppSwitchReturn].
6357 */
6458 @Volatile
6559 private var isHandlingReturnToApp = false
6660
67- /* *
68- * Set to `true` when [launchApp] starts an app-switch checkout so we only accept the following
69- * popupbridgev1 deep link as that flow's return (ignores stale links). Cleared when
70- * [handleAppSwitchReturn] runs or when launch falls back to the browser.
71- */
72- @Volatile
73- private var expectingAppSwitchReturn = false
61+ private val appSwitchHandler = AppSwitchHandler (
62+ activityRef = activityRef,
63+ analyticsClient = analyticsClient,
64+ onOpenUrl = { url -> openUrl(url) },
65+ onError = { e -> errorListener?.onError(e) },
66+ onCanceled = { runCanceledJavaScript() },
67+ onComplete = { uri -> runNotifyCompleteJavaScript(uri) },
68+ )
7469
7570 /* *
7671 * Create a new instance of [PopupBridgeClient].
@@ -122,7 +117,7 @@ private val enablePopupBridgeAppSwitch: Boolean = false,
122117
123118 with (popupBridgeJavascriptInterface) {
124119 onOpen = { url -> openUrl(url) }
125- onLaunchApp = { url -> this @PopupBridgeClient .launchApp(url) }
120+ onLaunchApp = { url -> appSwitchHandler .launchApp(url) }
126121 onSendMessage = { messageName, data ->
127122 messageListener?.onMessageReceived(messageName, data)
128123 }
@@ -143,8 +138,8 @@ private val enablePopupBridgeAppSwitch: Boolean = false,
143138
144139 fun handleReturnToApp (intent : Intent ) {
145140 val returnUri = intent.data
146- if (enablePopupBridgeAppSwitch && returnUri != null && returnUri.isAppSwitchReturnUri( )) {
147- handleAppSwitchReturn (returnUri)
141+ if (enablePopupBridgeAppSwitch && appSwitchHandler.shouldHandleReturn(returnUri )) {
142+ appSwitchHandler.handleReturn (returnUri!! )
148143 return
149144 }
150145
@@ -169,116 +164,15 @@ private val enablePopupBridgeAppSwitch: Boolean = false,
169164 )
170165 is BrowserSwitchFinalResult .NoResult -> runCanceledJavaScript()
171166 }
172- clearPopupBridgeReturnIntentIfPresent( " browser_switch_result_consumed " )
167+ appSwitchHandler.clearReturnIntentIfPresent( )
173168 isHandlingReturnToApp = false
174169 }
175170 }
176171
177- /* *
178- * Entry point when the web page calls window.popupBridge.launchApp(url).
179- * Posts to the main thread so [startActivity] is never called from a background thread
180- * (JavascriptInterface callbacks can be invoked off the main thread).
181- */
182- private fun launchApp (url : String? ) {
183- val activity = activityRef.get() as ? ComponentActivity ? : run {
184- return
185- }
186- mainHandler.post { launchAppAssumingMainThread(url, activity) }
187- }
188-
189- private fun launchAppAssumingMainThread (url : String? , activity : ComponentActivity ) {
190- if (url.isNullOrBlank()) {
191- errorListener?.onError(IllegalArgumentException (" Invalid URL for app launch" ))
192- return
193- }
194-
195- clearPopupBridgeReturnIntentIfPresent(" launching_new_app_switch" )
196- expectingAppSwitchReturn = true
197-
198- val uri = url?.toUri() ? : run {
199- errorListener?.onError(IllegalArgumentException (" Invalid URL for app launch" ))
200- return
201- }
202-
203- val intent = Intent (Intent .ACTION_VIEW , uri).apply {
204- addFlags(Intent .FLAG_ACTIVITY_NEW_TASK )
205- if (uri.isPayPalAppSwitchUri()) {
206- setPackage(PAYPAL_APP_PACKAGE )
207- }
208- }
209-
210- try {
211- activity.startActivity(intent)
212- analyticsClient.sendEvent(PopupBridgeAnalytics .POPUP_BRIDGE_APP_LAUNCHED )
213- } catch (_: ActivityNotFoundException ) {
214- expectingAppSwitchReturn = false
215- analyticsClient.sendEvent(PopupBridgeAnalytics .POPUP_BRIDGE_APP_LAUNCH_FAILED )
216- openUrl(url)
217- }
218- }
219-
220- private fun Uri.isPayPalAppSwitchUri (): Boolean {
221- val normalizedHost = host?.removePrefix(" www." )
222- return scheme.equals(" https" , ignoreCase = true ) &&
223- normalizedHost == " paypal.com" &&
224- path.orEmpty().startsWith(" /app-switch-checkout" )
225- }
226-
227- private fun Uri.isAppSwitchReturnUri (): Boolean {
228- if (host != POPUP_BRIDGE_URL_HOST ) {
229- return false
230- }
231-
232- if (! fragment.isNullOrBlank()) {
233- return true
234- }
235-
236- return hasAppSwitchPath()
237- }
238-
239- private fun Uri.isCancelUri (): Boolean {
240- val normalizedPath = path.orEmpty().lowercase()
241- return normalizedPath.contains(" oncancel" ) ||
242- normalizedPath.contains(" /cancel" )
243- }
244-
245- private fun Uri.hasAppSwitchPath (): Boolean {
246- val normalizedPath = path.orEmpty().lowercase()
247- return normalizedPath.contains(" onapprove" ) ||
248- normalizedPath.contains(" onerror" ) ||
249- normalizedPath.contains(" /approve" ) ||
250- normalizedPath.contains(" /error" ) ||
251- isCancelUri()
252- }
253-
254- /* *
255- * Handles the return deep link from a native PayPal/Venmo app switch.
256- *
257- * Runs only when [expectingAppSwitchReturn] is true (set by [launchApp] before starting the
258- * native app) so stale popupbridgev1 links do not complete or cancel the wrong session.
259- */
260- private fun handleAppSwitchReturn (returnUri : Uri ) {
261- if (! expectingAppSwitchReturn) {
262- return
263- }
264- expectingAppSwitchReturn = false
265-
266- analyticsClient.sendEvent(PopupBridgeAnalytics .POPUP_BRIDGE_APP_SWITCH_RETURNED )
267- clearPopupBridgeReturnIntentIfPresent(" app_switch_return_consumed" )
268-
269- if (returnUri.isCancelUri()) {
270- runCanceledJavaScript()
271- } else {
272- runNotifyCompleteJavaScript(returnUri)
273- }
274- }
275-
276172 private fun openUrl (url : String? ) {
277173 analyticsClient.sendEvent(PopupBridgeAnalytics .POPUP_BRIDGE_STARTED )
278174
279- val activity = activityRef.get() ? : run {
280- return
281- }
175+ val activity = activityRef.get() ? : return
282176 val browserSwitchOptions = BrowserSwitchOptions ()
283177 .requestCode(REQUEST_CODE )
284178 .url(url?.toUri())
@@ -360,20 +254,6 @@ private val enablePopupBridgeAppSwitch: Boolean = false,
360254 )
361255 }
362256
363- private fun clearPopupBridgeReturnIntentIfPresent (reason : String ) {
364- val activity = activityRef.get() ? : return
365- val currentIntent = activity.intent ? : return
366- val currentData = currentIntent.data ? : return
367-
368- if (currentData.host != POPUP_BRIDGE_URL_HOST ) {
369- return
370- }
371-
372- activity.intent = Intent (currentIntent).apply {
373- data = null
374- }
375- }
376-
377257 private fun runJavaScriptInWebView (script : String ) {
378258 webViewRef.get()?.post(
379259 Runnable { webViewRef.get()?.evaluateJavascript(script, null ) }
0 commit comments