Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Main into v3 #103

Merged
merged 5 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@
* Upgrade Kotlin version to 1.9.10
* Upgrade to Android Gradle Plugin 8
* Change `BrowserSwitchClient#start` parameters and return type
* Change `BrowserSwitchClient#parseResult` parameters
* Change `BrowserSwitchClient#parseResult` parameters
* Remove `deliverResult`, `getResult`, `captureResult`, `clearActiveRequests`, `getResultFromCache`, and `deliverResultFromCache` from `BrowserSwitchClient`
* Add `BrowserSwitchRequest` and `BrowserSwitchPendingRequest`
* Convert `BrowserSwitchResult` to sealed class and add `BrowserSwitchResultInfo`
* Remove `BrowserSwitchStatus`
* Rename `parseResult()` to `completeRequest()`

## 2.7.0

* Add `appLinkUri` to `BrowserSwitchOptions` for Android App Link support

## 2.6.1

* Throw `BrowserSwitchException` when a browser is not found to start browser switch
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Add the library to your dependencies in your `build.gradle`:

```groovy
dependencies {
implementation 'com.braintreepayments.api:browser-switch:2.6.1'
implementation 'com.braintreepayments.api:browser-switch:2.7.0'
}
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public BrowserSwitchPendingRequest start(@NonNull ComponentActivity activity, @N
Uri browserSwitchUrl = browserSwitchOptions.getUrl();
int requestCode = browserSwitchOptions.getRequestCode();
String returnUrlScheme = browserSwitchOptions.getReturnUrlScheme();
Uri appLinkUri = browserSwitchOptions.getAppLinkUri();

JSONObject metadata = browserSwitchOptions.getMetadata();

Expand All @@ -70,7 +71,7 @@ public BrowserSwitchPendingRequest start(@NonNull ComponentActivity activity, @N
BrowserSwitchRequest request;
try {
request =
new BrowserSwitchRequest(requestCode, browserSwitchUrl, metadata, returnUrlScheme, true);
new BrowserSwitchRequest(requestCode, browserSwitchUrl, metadata, returnUrlScheme, appLinkUri, true);
customTabsInternalClient.launchUrl(activity, browserSwitchUrl, launchAsNewTask);
} catch (ActivityNotFoundException e) {
return new BrowserSwitchPendingRequest.Failure(new BrowserSwitchException("Unable to start browser switch without a web browser."));
Expand Down Expand Up @@ -99,9 +100,10 @@ public void assertCanPerformBrowserSwitch(

if (!isValidRequestCode(requestCode)) {
errorMessage = activity.getString(R.string.error_request_code_invalid);
} else if (returnUrlScheme == null) {
errorMessage = activity.getString(R.string.error_return_url_required);
} else if (!browserSwitchInspector.isDeviceConfiguredForDeepLinking(appContext, returnUrlScheme)) {
} else if (returnUrlScheme == null && browserSwitchOptions.getAppLinkUri() == null) {
errorMessage = activity.getString(R.string.error_app_link_uri_or_return_url_required);
} else if (returnUrlScheme != null &&
!browserSwitchInspector.isDeviceConfiguredForDeepLinking(appContext, returnUrlScheme)) {
errorMessage = activity.getString(R.string.error_device_not_configured_for_deep_link);
}

Expand All @@ -128,9 +130,11 @@ private boolean isValidRequestCode(int requestCode) {
*/
public BrowserSwitchResult completeRequest(@NonNull BrowserSwitchPendingRequest.Started pendingRequest, @Nullable Intent intent) {
if (intent != null && intent.getData() != null) {
Uri deepLinkUrl = intent.getData();
if (pendingRequest.getBrowserSwitchRequest().matchesDeepLinkUrlScheme(deepLinkUrl)) {
BrowserSwitchResultInfo resultInfo = new BrowserSwitchResultInfo(pendingRequest.getBrowserSwitchRequest(), deepLinkUrl);
Uri linkUrl = intent.getData();
BrowserSwitchRequest request = pendingRequest.getBrowserSwitchRequest();
if (linkUrl != null &&
(request.matchesDeepLinkUrlScheme(linkUrl) || request.matchesAppLinkUri(linkUrl))) {
BrowserSwitchResultInfo resultInfo = new BrowserSwitchResultInfo(pendingRequest.getBrowserSwitchRequest(), linkUrl);
return new BrowserSwitchResult.Success(resultInfo);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public class BrowserSwitchOptions {
private int requestCode;
private Uri url;
private String returnUrlScheme;
private Uri appLinkUri;

private boolean launchAsNewTask;

Expand Down Expand Up @@ -66,6 +67,18 @@ public BrowserSwitchOptions returnUrlScheme(@Nullable String returnUrlScheme) {
return this;
}

/**
* Set App Link [Uri].
*
* @param appLinkUri The [Uri] containing the App Link URL used for navigating back into the application
* after browser switch
* @return {@link BrowserSwitchOptions} reference to instance to allow setter invocations to be chained
*/
public BrowserSwitchOptions appLinkUri(@Nullable Uri appLinkUri) {
this.appLinkUri = appLinkUri;
return this;
}

/**
* @return The metadata associated with the browser switch request
*/
Expand Down Expand Up @@ -97,6 +110,14 @@ public String getReturnUrlScheme() {
return returnUrlScheme;
}

/**
* @return The App Link [Uri] set for navigating back into the application after browser switch
*/
@Nullable
public Uri getAppLinkUri() {
return appLinkUri;
}

public boolean isLaunchAsNewTask() {
return launchAsNewTask;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,62 @@
import android.net.Uri;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;

import org.json.JSONException;
import org.json.JSONObject;

public class BrowserSwitchRequest {


private final Uri url;
private final int requestCode;
private final JSONObject metadata;
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public final String returnUrlScheme;
private Uri appLinkUri;
private boolean shouldNotifyCancellation;

@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public static BrowserSwitchRequest fromJson(String json) throws JSONException {
JSONObject jsonObject = new JSONObject(json);
int requestCode = jsonObject.getInt("requestCode");
String url = jsonObject.getString("url");
String returnUrlScheme = jsonObject.getString("returnUrlScheme");
JSONObject metadata = jsonObject.optJSONObject("metadata");
Uri appLinkUri = null;
if (jsonObject.has("appLinkUri")) {
appLinkUri = Uri.parse(jsonObject.getString("appLinkUri"));
}
String returnUrlScheme = null;
if (jsonObject.has("returnUrlScheme")) {
returnUrlScheme = jsonObject.getString("returnUrlScheme");
}
boolean shouldNotify = jsonObject.optBoolean("shouldNotify", true);
return new BrowserSwitchRequest(requestCode, Uri.parse(url), metadata, returnUrlScheme, shouldNotify);
return new BrowserSwitchRequest(
requestCode,
Uri.parse(url),
metadata,
returnUrlScheme,
appLinkUri,
shouldNotify
);
}

@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
public BrowserSwitchRequest(int requestCode, Uri url, JSONObject metadata, String returnUrlScheme, boolean shouldNotifyCancellation) {
public BrowserSwitchRequest(
int requestCode,
Uri url,
JSONObject metadata,
String returnUrlScheme,
Uri appLinkUri,
boolean shouldNotifyCancellation
) {
this.url = url;
this.requestCode = requestCode;
this.metadata = metadata;
this.returnUrlScheme = returnUrlScheme;
this.appLinkUri = appLinkUri;
this.shouldNotifyCancellation = shouldNotifyCancellation;
}

Expand Down Expand Up @@ -62,6 +86,17 @@ void setShouldNotifyCancellation(boolean shouldNotifyCancellation) {
this.shouldNotifyCancellation = shouldNotifyCancellation;
}

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Nullable
public Uri getAppLinkUri() {
return appLinkUri;
}

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public void setAppLinkUri(@Nullable Uri appLinkUri) {
this.appLinkUri = appLinkUri;
}

String toJson() throws JSONException {
JSONObject result = new JSONObject();
result.put("requestCode", requestCode);
Expand All @@ -71,10 +106,19 @@ String toJson() throws JSONException {
if (metadata != null) {
result.put("metadata", metadata);
}
if (appLinkUri != null) {
result.put("appLinkUri", appLinkUri.toString());
}
return result.toString();
}

boolean matchesDeepLinkUrlScheme(@NonNull Uri url) {
return url.getScheme() != null && url.getScheme().equalsIgnoreCase(returnUrlScheme);
}

boolean matchesAppLinkUri(@NonNull Uri uri) {
return appLinkUri != null &&
uri.getScheme().equals(appLinkUri.getScheme()) &&
uri.getHost().equals(appLinkUri.getHost());
}
}
2 changes: 1 addition & 1 deletion browser-switch/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="error_request_code_invalid">Request code cannot be Integer.MIN_VALUE</string>
<string name="error_return_url_required">A returnUrlScheme is required.</string>
<string name="error_app_link_uri_or_return_url_required">An appLinkUri or returnUrlScheme is required.</string>
<string name="error_browser_not_found">No installed activities can open this URL: %1$s</string>
<string name="error_device_not_configured_for_deep_link">The return url scheme was not set up, incorrectly set up, or more than one Activity on this device defines the same url scheme in it\'s Android Manifest. See https://github.com/braintree/browser-switch-android for more information on setting up a return url scheme.</string>
</resources>
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package com.braintreepayments.api;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
Expand All @@ -18,11 +22,13 @@
import android.net.Uri;

import androidx.activity.ComponentActivity;
import androidx.fragment.app.FragmentActivity;

import org.json.JSONObject;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.android.controller.ActivityController;
Expand All @@ -35,6 +41,7 @@ public class BrowserSwitchClientUnitTest {
private ChromeCustomTabsInternalClient customTabsInternalClient;

private Uri browserSwitchDestinationUrl;
private Uri appLinkUri;
private Context applicationContext;

private ComponentActivity componentActivity;
Expand All @@ -45,6 +52,7 @@ public void beforeEach() {
customTabsInternalClient = mock(ChromeCustomTabsInternalClient.class);

browserSwitchDestinationUrl = Uri.parse("https://example.com/browser_switch_destination");
appLinkUri = Uri.parse("https://example.com");

ActivityController<ComponentActivity> componentActivityController =
Robolectric.buildActivity(ComponentActivity.class).setup();
Expand Down Expand Up @@ -160,7 +168,7 @@ public void start_whenDeviceIsNotConfiguredForDeepLinking_returnsFailure() {
}

@Test
public void start_whenNoReturnUrlSchemeSet_throwsFailure() {
public void start_whenNoAppLinkUriOrReturnUrlSchemeSet_throwsError() {
when(browserSwitchInspector.isDeviceConfiguredForDeepLinking(applicationContext, "return-url-scheme")).thenReturn(true);

BrowserSwitchClient sut = new BrowserSwitchClient(browserSwitchInspector,
Expand All @@ -170,21 +178,51 @@ public void start_whenNoReturnUrlSchemeSet_throwsFailure() {
BrowserSwitchOptions options = new BrowserSwitchOptions()
.requestCode(123)
.returnUrlScheme(null)
.appLinkUri(null)
.url(browserSwitchDestinationUrl)
.metadata(metadata);
BrowserSwitchPendingRequest request = sut.start(componentActivity, options);
assertTrue(request instanceof BrowserSwitchPendingRequest.Failure);
assertEquals("A returnUrlScheme is required.", ((BrowserSwitchPendingRequest.Failure) request).getCause().getMessage());
assertEquals("An appLinkUri or returnUrlScheme is required.", ((BrowserSwitchPendingRequest.Failure) request).getCause().getMessage());
}

@Test
public void completeRequest_whenAppLinkMatches_successReturnedWithAppLink() {
Uri appLinkUri = Uri.parse("https://example.com");
BrowserSwitchClient sut = new BrowserSwitchClient(browserSwitchInspector,
customTabsInternalClient);

JSONObject requestMetadata = new JSONObject();
BrowserSwitchRequest request = new BrowserSwitchRequest(
123,
browserSwitchDestinationUrl,
requestMetadata,
null,
appLinkUri,
false
);

Intent intent = new Intent(Intent.ACTION_VIEW, appLinkUri);
BrowserSwitchResult browserSwitchResult = sut.completeRequest(new BrowserSwitchPendingRequest.Started(request), intent);

assertTrue(browserSwitchResult instanceof BrowserSwitchResult.Success);
assertEquals(appLinkUri, ((BrowserSwitchResult.Success) browserSwitchResult).getResultInfo().getDeepLinkUrl());
}

@Test
public void completeRequest_whenActiveRequestMatchesDeepLinkResultURLScheme_returnsBrowserSwitchSuccessResult() {
BrowserSwitchClient sut = new BrowserSwitchClient(browserSwitchInspector,
customTabsInternalClient);
customTabsInternalClient);

JSONObject requestMetadata = new JSONObject();
BrowserSwitchRequest request =
new BrowserSwitchRequest(123, browserSwitchDestinationUrl, requestMetadata, "fake-url-scheme", false);
BrowserSwitchRequest request = new BrowserSwitchRequest(
123,
browserSwitchDestinationUrl,
requestMetadata,
"fake-url-scheme",
null,
false
);

Uri deepLinkUrl = Uri.parse("fake-url-scheme://success");
Intent intent = new Intent(Intent.ACTION_VIEW, deepLinkUrl);
Expand All @@ -200,8 +238,14 @@ public void completeRequest_whenDeepLinkResultURLSchemeDoesntMatch_returnsNoResu
customTabsInternalClient);

JSONObject requestMetadata = new JSONObject();
BrowserSwitchRequest request =
new BrowserSwitchRequest(123, browserSwitchDestinationUrl, requestMetadata, "fake-url-scheme", false);
BrowserSwitchRequest request = new BrowserSwitchRequest(
123,
browserSwitchDestinationUrl,
requestMetadata,
"fake-url-scheme",
null,
false
);

Uri deepLinkUrl = Uri.parse("a-different-url-scheme://success");
Intent intent = new Intent(Intent.ACTION_VIEW, deepLinkUrl);
Expand All @@ -217,7 +261,7 @@ public void completeRequest_whenIntentIsNull_returnsNoResult() {

JSONObject requestMetadata = new JSONObject();
BrowserSwitchRequest request =
new BrowserSwitchRequest(123, browserSwitchDestinationUrl, requestMetadata, "fake-url-scheme", false);
new BrowserSwitchRequest(123, browserSwitchDestinationUrl, requestMetadata, "fake-url-scheme", null, false);

BrowserSwitchResult browserSwitchResult = sut.completeRequest(new BrowserSwitchPendingRequest.Started(request), null);
assertTrue(browserSwitchResult instanceof BrowserSwitchResult.NoResult);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class BrowserSwitchPendingRequestUnitTest {
Uri.parse("http://"),
JSONObject().put("test_key", "test_value"),
"return-url-scheme",
Uri.parse("https://example.com"),
false
)

Expand Down
Loading
Loading