Skip to content

Commit 2921221

Browse files
authored
Fixing Authtab for applinks and App switch (#125)
* fix Applink return URL by providing a successAppLinkUri * add forceChromeCustomTab boolean to ensure correct flow for app switch * add forceChromeCustomTab boolean to ensure correct flow for app switch * add a CHANGELOG.md entry * add a CHANGELOG.md entry * fix tests * adding a test to ensure forceChromeCustomTab boolean controls the flow correctly
1 parent 645b9af commit 2921221

File tree

7 files changed

+109
-23
lines changed

7 files changed

+109
-23
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# browser-switch-android Release Notes
22

3+
## Unreleased
4+
5+
* Add `forceChromeCustomTabs` boolean parameter to `BrowserSwitchClient.start()`
6+
* Provide control over browser switch flow when there is a need to support switching between applications
7+
* Add `successAppLinkUri` to `BrowserSwitchOptions`
8+
* Fix App Link return URL by providing a dedicated URI with full path for returning to the application
9+
310
## 3.3.0
411

512
* Add AuthTab Support

browser-switch/src/main/java/com/braintreepayments/api/AuthTabInternalClient.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,19 +31,21 @@ internal class AuthTabInternalClient (
3131
url: Uri,
3232
returnUrlScheme: String?,
3333
appLinkUri: Uri?,
34+
successAppLinkUri: Uri?,
3435
launcher: ActivityResultLauncher<Intent>?,
35-
launchType: LaunchType?
36+
launchType: LaunchType?,
37+
forceChromeCustomTab: Boolean = false
3638
) {
37-
val useAuthTab = isAuthTabSupported(context)
39+
val useAuthTab = isAuthTabSupported(context) && !forceChromeCustomTab
3840

3941
if (useAuthTab && launcher != null) {
4042
val authTabIntent = authTabIntentBuilder.build()
4143

4244
if (launchType == LaunchType.ACTIVITY_CLEAR_TOP) {
4345
authTabIntent.intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
4446
}
45-
appLinkUri?.host?.let { host ->
46-
val path = appLinkUri.path ?: "/"
47+
successAppLinkUri?.host?.let { host ->
48+
val path = successAppLinkUri.path ?: "/"
4749
authTabIntent.launch(launcher, url, host, path)
4850
} ?: returnUrlScheme?.let {
4951
authTabIntent.launch(launcher, url, returnUrlScheme)

browser-switch/src/main/java/com/braintreepayments/api/BrowserSwitchClient.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,18 +142,28 @@ public void restorePendingRequest(@NonNull String pendingRequest) throws Browser
142142
this.pendingAuthTabRequest = BrowserSwitchRequest.fromBase64EncodedJSON(pendingRequest);
143143
}
144144

145+
/**
146+
* A method to provide backwards compatibility
147+
*/
148+
@NonNull
149+
public BrowserSwitchStartResult start(@NonNull Activity activity,
150+
@NonNull BrowserSwitchOptions browserSwitchOptions) {
151+
return start(activity, browserSwitchOptions, false);
152+
}
145153
/**
146154
* Open a browser or Auth Tab with a given set of {@link BrowserSwitchOptions} from an Android activity.
147155
*
148156
* @param activity the activity used to start browser switch
149157
* @param browserSwitchOptions {@link BrowserSwitchOptions} the options used to configure the browser switch
158+
* @param forceChromeCustomTabs boolean to ensure correct flow for applications that need to support sending users to another app and back
150159
* @return a {@link BrowserSwitchStartResult.Started} that should be stored and passed to
151160
* {@link BrowserSwitchClient#completeRequest(Intent, String)} upon return to the app (for Custom Tabs fallback),
152161
* or {@link BrowserSwitchStartResult.Failure} if browser could not be launched.
153162
*/
154163
@NonNull
155164
public BrowserSwitchStartResult start(@NonNull Activity activity,
156-
@NonNull BrowserSwitchOptions browserSwitchOptions) {
165+
@NonNull BrowserSwitchOptions browserSwitchOptions,
166+
boolean forceChromeCustomTabs) {
157167

158168
this.authTabCallbackResult = null;
159169

@@ -167,6 +177,8 @@ public BrowserSwitchStartResult start(@NonNull Activity activity,
167177
int requestCode = browserSwitchOptions.getRequestCode();
168178
String returnUrlScheme = browserSwitchOptions.getReturnUrlScheme();
169179
Uri appLinkUri = browserSwitchOptions.getAppLinkUri();
180+
Uri successAppLinkUri = browserSwitchOptions.getSuccessAppLinkUri();
181+
170182
JSONObject metadata = browserSwitchOptions.getMetadata();
171183

172184
if (activity.isFinishing()) {
@@ -187,7 +199,7 @@ public BrowserSwitchStartResult start(@NonNull Activity activity,
187199
appLinkUri
188200
);
189201

190-
boolean useAuthTab = isAuthTabSupported(activity);
202+
boolean useAuthTab = isAuthTabSupported(activity) && !forceChromeCustomTabs;
191203

192204
if (useAuthTab) {
193205
this.pendingAuthTabRequest = request;
@@ -198,8 +210,10 @@ public BrowserSwitchStartResult start(@NonNull Activity activity,
198210
browserSwitchUrl,
199211
returnUrlScheme,
200212
appLinkUri,
213+
successAppLinkUri,
201214
authTabLauncher,
202-
launchType
215+
launchType,
216+
forceChromeCustomTabs
203217
);
204218

205219
return new BrowserSwitchStartResult.Started(request.toBase64EncodedJSON());

browser-switch/src/main/java/com/braintreepayments/api/BrowserSwitchOptions.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public class BrowserSwitchOptions {
1717
private Uri url;
1818
private String returnUrlScheme;
1919
private Uri appLinkUri;
20+
private Uri successAppLinkUri;
2021

2122
private boolean launchAsNewTask;
2223
private LaunchType launchType;
@@ -78,6 +79,17 @@ public BrowserSwitchOptions appLinkUri(@Nullable Uri appLinkUri) {
7879
this.appLinkUri = appLinkUri;
7980
return this;
8081
}
82+
/**
83+
* Set Success App Link [Uri].
84+
*
85+
* @param successAppLinkUri The [Uri] containing the App Link URL with full success path
86+
* used for navigating back into the application after browser switch
87+
* @return {@link BrowserSwitchOptions} reference to instance to allow setter invocations to be chained
88+
*/
89+
public BrowserSwitchOptions successAppLinkUri(@Nullable Uri successAppLinkUri) {
90+
this.successAppLinkUri = successAppLinkUri;
91+
return this;
92+
}
8193

8294
/**
8395
* @return The metadata associated with the browser switch request
@@ -118,6 +130,15 @@ public Uri getAppLinkUri() {
118130
return appLinkUri;
119131
}
120132

133+
/**
134+
* @return The Success App Link [Uri] set for navigating back into the application
135+
* after browser switch containing the full path back
136+
*/
137+
@Nullable
138+
public Uri getSuccessAppLinkUri() {
139+
return successAppLinkUri;
140+
}
141+
121142
/**
122143
* @deprecated Use {@link #getLaunchType()} instead.
123144
*/

browser-switch/src/test/java/com/braintreepayments/api/AuthTabInternalClientUnitTest.kt

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,15 @@ class AuthTabInternalClientUnitTest {
7777
@Test
7878
fun `launchUrl uses Auth Tab with app link when supported`() {
7979
val appLinkUri = Uri.parse("https://example.com/auth")
80+
val successAppLinkUri = Uri.parse("https://example.com/auth")
8081
val packageName = "com.android.chrome"
8182
customTabsIntent = mockk(relaxed = true)
8283
every { CustomTabsClient.getPackageName(context, null) } returns packageName
8384
every { CustomTabsClient.isAuthTabSupported(context, packageName) } returns true
8485

8586
val sut = AuthTabInternalClient(authTabBuilder, customTabsBuilder)
8687

87-
sut.launchUrl(context, url, null, appLinkUri, launcher, null)
88+
sut.launchUrl(context, url, null, appLinkUri, successAppLinkUri, launcher, null, false)
8889

8990
verify {
9091
authTabIntent.launch(launcher, url, "example.com", "/auth")
@@ -104,7 +105,7 @@ class AuthTabInternalClientUnitTest {
104105

105106
val sut = AuthTabInternalClient(authTabBuilder, customTabsBuilder)
106107

107-
sut.launchUrl(context, url, returnUrlScheme, null, launcher, null)
108+
sut.launchUrl(context, url, returnUrlScheme, null, null, launcher, null, false)
108109

109110
verify {
110111
authTabIntent.launch(launcher, url, returnUrlScheme)
@@ -125,7 +126,7 @@ class AuthTabInternalClientUnitTest {
125126
val sut = AuthTabInternalClient(authTabBuilder, customTabsBuilder)
126127
val intent = authTabIntent.intent
127128

128-
sut.launchUrl(context, url, returnUrlScheme, null, launcher, LaunchType.ACTIVITY_CLEAR_TOP)
129+
sut.launchUrl(context, url, returnUrlScheme, null, null, launcher, LaunchType.ACTIVITY_CLEAR_TOP, false)
129130

130131
assertTrue(intent.flags and Intent.FLAG_ACTIVITY_CLEAR_TOP != 0)
131132

@@ -147,7 +148,31 @@ class AuthTabInternalClientUnitTest {
147148

148149
val sut = AuthTabInternalClient(authTabBuilder, customTabsBuilder)
149150

150-
sut.launchUrl(context, url, returnUrlScheme, null, launcher, null)
151+
sut.launchUrl(context, url, returnUrlScheme, null, null, launcher, null, false)
152+
153+
verify {
154+
customTabsIntent.launchUrl(context, url)
155+
}
156+
verify(exactly = 0) {
157+
authTabIntent.launch(any(), any(), any<String>())
158+
}
159+
}
160+
161+
@Test
162+
fun `launchUrl falls back to Custom Tabs when forceChromeCustomTab is true`() {
163+
val appLinkUri = Uri.parse("https://example.com/auth")
164+
val successAppLinkUri = Uri.parse("https://example.com/auth")
165+
val packageName = "com.android.chrome"
166+
val returnUrlScheme = "example"
167+
authTabIntent = mockk(relaxed = true)
168+
every { authTabBuilder.build() } returns authTabIntent
169+
170+
every { CustomTabsClient.getPackageName(context, null) } returns packageName
171+
every { CustomTabsClient.isAuthTabSupported(context, packageName) } returns true
172+
173+
val sut = AuthTabInternalClient(authTabBuilder, customTabsBuilder)
174+
175+
sut.launchUrl(context, url, returnUrlScheme, appLinkUri, successAppLinkUri, launcher, null, true)
151176

152177
verify {
153178
customTabsIntent.launchUrl(context, url)
@@ -160,14 +185,15 @@ class AuthTabInternalClientUnitTest {
160185
@Test
161186
fun `launchUrl handles app link with no path`() {
162187
val appLinkUri = Uri.parse("https://example.com")
188+
val successAppLinkUri = Uri.parse("https://example.com")
163189
val packageName = "com.android.chrome"
164190

165191
every { CustomTabsClient.getPackageName(context, null) } returns packageName
166192
every { CustomTabsClient.isAuthTabSupported(context, packageName) } returns true
167193

168194
val sut = AuthTabInternalClient(authTabBuilder, customTabsBuilder)
169195

170-
sut.launchUrl(context, url, null, appLinkUri, launcher, null)
196+
sut.launchUrl(context, url, null, appLinkUri, successAppLinkUri, launcher, null, false)
171197

172198
verify {
173199
authTabIntent.launch(launcher, url, "example.com", "/")
@@ -183,7 +209,7 @@ class AuthTabInternalClientUnitTest {
183209
// Force AuthTab not to be supported to fall back to Custom Tabs
184210
every { CustomTabsClient.getPackageName(context, null) } returns null
185211

186-
sut.launchUrl(context, url, returnUrlScheme, null, launcher, null)
212+
sut.launchUrl(context, url, returnUrlScheme, null, null, launcher, null, false)
187213

188214
assertEquals(0, intent.flags)
189215
}
@@ -197,7 +223,7 @@ class AuthTabInternalClientUnitTest {
197223
// Force AuthTab not to be supported to fall back to Custom Tabs
198224
every { CustomTabsClient.getPackageName(context, null) } returns null
199225

200-
sut.launchUrl(context, url, returnUrlScheme, null, launcher, LaunchType.ACTIVITY_NEW_TASK)
226+
sut.launchUrl(context, url, returnUrlScheme, null, null, launcher, LaunchType.ACTIVITY_NEW_TASK, false)
201227

202228
assertTrue(intent.flags and Intent.FLAG_ACTIVITY_NEW_TASK != 0)
203229
}
@@ -211,7 +237,7 @@ class AuthTabInternalClientUnitTest {
211237
// Force AuthTab not to be supported to fall back to Custom Tabs
212238
every { CustomTabsClient.getPackageName(context, null) } returns null
213239

214-
sut.launchUrl(context, url, returnUrlScheme, null, launcher, LaunchType.ACTIVITY_CLEAR_TOP)
240+
sut.launchUrl(context, url, returnUrlScheme, null, null, launcher, LaunchType.ACTIVITY_CLEAR_TOP, false)
215241

216242
assertTrue(intent.flags and Intent.FLAG_ACTIVITY_CLEAR_TOP != 0)
217243
}
@@ -225,7 +251,7 @@ class AuthTabInternalClientUnitTest {
225251
val sut = AuthTabInternalClient(authTabBuilder, customTabsBuilder)
226252
val returnUrlScheme = "example"
227253

228-
sut.launchUrl(context, url, returnUrlScheme, null, null, null)
254+
sut.launchUrl(context, url, returnUrlScheme, null, null, null, null, false)
229255

230256
verify {
231257
customTabsIntent.launchUrl(context, url)

browser-switch/src/test/java/com/braintreepayments/api/BrowserSwitchClientUnitTest.java

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,9 @@ public void start_whenSuccessful_returnsBrowserSwitchRequest() throws BrowserSwi
111111
eq("return-url-scheme"),
112112
isNull(),
113113
isNull(),
114-
eq(LaunchType.ACTIVITY_CLEAR_TOP)
114+
isNull(),
115+
eq(LaunchType.ACTIVITY_CLEAR_TOP),
116+
eq(false)
115117
);
116118

117119
assertNotNull(browserSwitchPendingRequest);
@@ -130,7 +132,7 @@ public void start_whenSuccessful_returnsBrowserSwitchRequest() throws BrowserSwi
130132
@Test
131133
public void start_withAppLinkUri_passesItToAuthTab() {
132134
Uri appLinkUri = Uri.parse("https://example.com/auth");
133-
135+
Uri successAppLinkUri = Uri.parse("https://example.com/auth");
134136
BrowserSwitchClient sut = new BrowserSwitchClient(browserSwitchInspector,
135137
authTabInternalClient);
136138

@@ -139,6 +141,7 @@ public void start_withAppLinkUri_passesItToAuthTab() {
139141
.requestCode(123)
140142
.url(browserSwitchDestinationUrl)
141143
.appLinkUri(appLinkUri)
144+
.successAppLinkUri(successAppLinkUri)
142145
.metadata(metadata);
143146

144147
BrowserSwitchStartResult browserSwitchPendingRequest = sut.start(componentActivity, options);
@@ -148,8 +151,10 @@ public void start_withAppLinkUri_passesItToAuthTab() {
148151
eq(browserSwitchDestinationUrl),
149152
isNull(),
150153
eq(appLinkUri),
154+
eq(successAppLinkUri),
155+
isNull(),
151156
isNull(),
152-
isNull()
157+
eq(false)
153158
);
154159

155160
assertTrue(browserSwitchPendingRequest instanceof BrowserSwitchStartResult.Started);
@@ -165,7 +170,9 @@ public void start_whenNoBrowserAvailable_returnsFailure() {
165170
eq("return-url-scheme"),
166171
isNull(),
167172
isNull(),
168-
isNull()
173+
isNull(),
174+
isNull(),
175+
eq(false)
169176
);
170177

171178
BrowserSwitchClient sut = new BrowserSwitchClient(browserSwitchInspector,
@@ -315,8 +322,10 @@ public void start_withAuthTabLauncherInitialized_usesPendingAuthTabRequest() thr
315322
eq(browserSwitchDestinationUrl),
316323
eq("return-url-scheme"),
317324
isNull(),
325+
isNull(),
318326
eq(mockLauncher),
319-
isNull()
327+
isNull(),
328+
eq(false)
320329
);
321330

322331
String pendingRequestString = ((BrowserSwitchStartResult.Started) result).getPendingRequest();
@@ -430,7 +439,9 @@ public void start_withoutAuthTabLauncher_fallsBackToCustomTabs() {
430439
eq("return-url-scheme"),
431440
isNull(),
432441
isNull(),
433-
isNull()
442+
isNull(),
443+
isNull(),
444+
eq(false)
434445
);
435446
}
436447

@@ -462,7 +473,9 @@ public void start_whenAuthTabLauncherIsNull_fallsBackToCustomTabs() {
462473
eq("return-url-scheme"),
463474
isNull(),
464475
isNull(),
465-
isNull()
476+
isNull(),
477+
isNull(),
478+
eq(false)
466479
);
467480
}
468481

demo/src/main/java/com/braintreepayments/api/browserswitch/demo/DemoFragment.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ private void startBrowserSwitch(@Nullable JSONObject metadata) {
8686
browserSwitchOptions.appLinkUri(
8787
Uri.parse("https://mobile-sdk-demo-site-838cead5d3ab.herokuapp.com")
8888
);
89+
browserSwitchOptions.successAppLinkUri(
90+
Uri.parse("https://mobile-sdk-demo-site-838cead5d3ab.herokuapp.com")
91+
);
8992
} else {
9093
browserSwitchOptions.returnUrlScheme(getReturnUrlScheme());
9194
}

0 commit comments

Comments
 (0)