From 2dfc5d94ed298a3c7e1ac25813614102693d7871 Mon Sep 17 00:00:00 2001 From: Tim Chow Date: Mon, 6 May 2024 10:39:41 -0500 Subject: [PATCH 1/6] Add appLinkUri param (#92) * Add appLinkUri to BrowserSwitchOptions and BrowserSwitchRequest * Add changelog entry * Fix spacing and javadoc --- CHANGELOG.md | 4 + .../api/BrowserSwitchClient.java | 3 +- .../api/BrowserSwitchOptions.java | 21 ++++ .../api/BrowserSwitchRequest.java | 40 +++++++- .../api/BrowserSwitchClientUnitTest.java | 97 ++++++++++++++++--- .../api/BrowserSwitchRequestUnitTest.java | 14 ++- .../api/BrowserSwitchResultUnitTest.java | 5 +- 7 files changed, 160 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b8eb69b..153db965 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # browser-switch-android Release Notes +## unreleased + +* Add `appLinkUri` to `BrowserSwitchOptions` for Android App Link support + ## 2.6.1 * Throw `BrowserSwitchException` when a browser is not found to start browser switch diff --git a/browser-switch/src/main/java/com/braintreepayments/api/BrowserSwitchClient.java b/browser-switch/src/main/java/com/braintreepayments/api/BrowserSwitchClient.java index ab3ab211..5e348cd1 100644 --- a/browser-switch/src/main/java/com/braintreepayments/api/BrowserSwitchClient.java +++ b/browser-switch/src/main/java/com/braintreepayments/api/BrowserSwitchClient.java @@ -53,10 +53,11 @@ public void start(@NonNull FragmentActivity activity, @NonNull BrowserSwitchOpti Uri browserSwitchUrl = browserSwitchOptions.getUrl(); int requestCode = browserSwitchOptions.getRequestCode(); String returnUrlScheme = browserSwitchOptions.getReturnUrlScheme(); + Uri appLinkUri = browserSwitchOptions.getAppLinkUri(); JSONObject metadata = browserSwitchOptions.getMetadata(); BrowserSwitchRequest request = - new BrowserSwitchRequest(requestCode, browserSwitchUrl, metadata, returnUrlScheme, true); + new BrowserSwitchRequest(requestCode, browserSwitchUrl, metadata, returnUrlScheme, appLinkUri, true); persistentStore.putActiveRequest(request, appContext); if (activity.isFinishing()) { diff --git a/browser-switch/src/main/java/com/braintreepayments/api/BrowserSwitchOptions.java b/browser-switch/src/main/java/com/braintreepayments/api/BrowserSwitchOptions.java index 75823c16..11080efb 100644 --- a/browser-switch/src/main/java/com/braintreepayments/api/BrowserSwitchOptions.java +++ b/browser-switch/src/main/java/com/braintreepayments/api/BrowserSwitchOptions.java @@ -19,6 +19,7 @@ public class BrowserSwitchOptions { private int requestCode; private Uri url; private String returnUrlScheme; + private Uri appLinkUri; private boolean launchAsNewTask; @@ -68,6 +69,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 */ @@ -99,6 +112,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; } diff --git a/browser-switch/src/main/java/com/braintreepayments/api/BrowserSwitchRequest.java b/browser-switch/src/main/java/com/braintreepayments/api/BrowserSwitchRequest.java index 77550ea6..1d492324 100644 --- a/browser-switch/src/main/java/com/braintreepayments/api/BrowserSwitchRequest.java +++ b/browser-switch/src/main/java/com/braintreepayments/api/BrowserSwitchRequest.java @@ -3,6 +3,8 @@ import android.net.Uri; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; import org.json.JSONException; import org.json.JSONObject; @@ -14,6 +16,7 @@ class BrowserSwitchRequest { private final int requestCode; private final JSONObject metadata; private final String returnUrlScheme; + private Uri appLinkUri; private boolean shouldNotifyCancellation; static BrowserSwitchRequest fromJson(String json) throws JSONException { @@ -22,15 +25,34 @@ static BrowserSwitchRequest fromJson(String json) throws JSONException { 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")); + } 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 + ); } - BrowserSwitchRequest(int requestCode, Uri url, JSONObject metadata, String returnUrlScheme, boolean shouldNotifyCancellation) { + 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; } @@ -54,6 +76,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); @@ -63,6 +96,9 @@ String toJson() throws JSONException { if (metadata != null) { result.put("metadata", metadata); } + if (appLinkUri != null) { + result.put("appLinkUri", appLinkUri.toString()); + } return result.toString(); } diff --git a/browser-switch/src/test/java/com/braintreepayments/api/BrowserSwitchClientUnitTest.java b/browser-switch/src/test/java/com/braintreepayments/api/BrowserSwitchClientUnitTest.java index 7b88219d..91d8b5a2 100644 --- a/browser-switch/src/test/java/com/braintreepayments/api/BrowserSwitchClientUnitTest.java +++ b/browser-switch/src/test/java/com/braintreepayments/api/BrowserSwitchClientUnitTest.java @@ -46,6 +46,7 @@ public class BrowserSwitchClientUnitTest { private ChromeCustomTabsInternalClient customTabsInternalClient; private Uri browserSwitchDestinationUrl; + private Uri appLinkUri; private Context applicationContext; private ActivityController activityController; @@ -59,6 +60,7 @@ public void beforeEach() { customTabsInternalClient = mock(ChromeCustomTabsInternalClient.class); browserSwitchDestinationUrl = Uri.parse("https://example.com/browser_switch_destination"); + appLinkUri = Uri.parse("https://example.com"); activityController = Robolectric.buildActivity(FragmentActivity.class).setup(); activity = spy(activityController.get()); @@ -271,7 +273,14 @@ public void deliverResult_whenDeepLinkUrlExists_AndReturnUrlSchemeDoesNotMatch_A when(activity.getIntent()).thenReturn(deepLinkIntent); JSONObject requestMetadata = new JSONObject(); - BrowserSwitchRequest request = new BrowserSwitchRequest(123, browserSwitchDestinationUrl, requestMetadata, "return-url-scheme", true); + BrowserSwitchRequest request = new BrowserSwitchRequest( + 123, + browserSwitchDestinationUrl, + requestMetadata, + "return-url-scheme", + appLinkUri, + true + ); when(persistentStore.getActiveRequest(applicationContext)).thenReturn(request); BrowserSwitchClient sut = new BrowserSwitchClient(browserSwitchInspector, persistentStore, customTabsInternalClient); @@ -301,7 +310,14 @@ public void deliverResult_whenDeepLinkUrlExistsAndReturnUrlSchemeDoesNotMatchAnd when(activity.getIntent()).thenReturn(deepLinkIntent); JSONObject requestMetadata = new JSONObject(); - BrowserSwitchRequest request = new BrowserSwitchRequest(123, browserSwitchDestinationUrl, requestMetadata, "return-url-scheme", false); + BrowserSwitchRequest request = new BrowserSwitchRequest( + 123, + browserSwitchDestinationUrl, + requestMetadata, + "return-url-scheme", + appLinkUri, + false + ); when(persistentStore.getActiveRequest(applicationContext)).thenReturn(request); BrowserSwitchClient sut = new BrowserSwitchClient(browserSwitchInspector, persistentStore, customTabsInternalClient); @@ -317,8 +333,14 @@ public void deliverResult_whenDeepLinkUrlDoesNotExistAndShouldNotifyCancellation when(activity.getIntent()).thenReturn(new Intent()); JSONObject requestMetadata = new JSONObject(); - BrowserSwitchRequest request = - new BrowserSwitchRequest(123, browserSwitchDestinationUrl, requestMetadata, "return-url-scheme", true); + BrowserSwitchRequest request = new BrowserSwitchRequest( + 123, + browserSwitchDestinationUrl, + requestMetadata, + "return-url-scheme", + appLinkUri, + true + ); when(persistentStore.getActiveRequest(applicationContext)).thenReturn(request); BrowserSwitchClient sut = new BrowserSwitchClient(browserSwitchInspector, persistentStore, customTabsInternalClient); @@ -345,8 +367,14 @@ public void deliverResult_whenDeepLinkUrlDoesNotExistAndShouldNotNotifyCancellat when(activity.getIntent()).thenReturn(new Intent()); JSONObject requestMetadata = new JSONObject(); - BrowserSwitchRequest request = - new BrowserSwitchRequest(123, browserSwitchDestinationUrl, requestMetadata, "return-url-scheme", false); + BrowserSwitchRequest request = new BrowserSwitchRequest( + 123, + browserSwitchDestinationUrl, + requestMetadata, + "return-url-scheme", + appLinkUri, + false + ); when(persistentStore.getActiveRequest(applicationContext)).thenReturn(request); BrowserSwitchClient sut = new BrowserSwitchClient(browserSwitchInspector, persistentStore, customTabsInternalClient); @@ -377,7 +405,14 @@ public void captureResult_whenDeepLinkUrlExistsAndReturnUrlSchemeMatches_storesS when(activity.getIntent()).thenReturn(deepLinkIntent); JSONObject requestMetadata = new JSONObject(); - BrowserSwitchRequest request = new BrowserSwitchRequest(123, browserSwitchDestinationUrl, requestMetadata, "return-url-scheme", false); + BrowserSwitchRequest request = new BrowserSwitchRequest( + 123, + browserSwitchDestinationUrl, + requestMetadata, + "return-url-scheme", + appLinkUri, + false + ); when(persistentStore.getActiveRequest(applicationContext)).thenReturn(request); BrowserSwitchClient sut = new BrowserSwitchClient(browserSwitchInspector, persistentStore, customTabsInternalClient); @@ -413,7 +448,14 @@ public void captureResult_whenIntentIsNull_doesNothing() { when(activity.getIntent()).thenReturn(null); JSONObject requestMetadata = new JSONObject(); - BrowserSwitchRequest request = new BrowserSwitchRequest(123, browserSwitchDestinationUrl, requestMetadata, "return-url-scheme", false); + BrowserSwitchRequest request = new BrowserSwitchRequest( + 123, + browserSwitchDestinationUrl, + requestMetadata, + "return-url-scheme", + appLinkUri, + false + ); when(persistentStore.getActiveRequest(applicationContext)).thenReturn(request); BrowserSwitchClient sut = new BrowserSwitchClient(browserSwitchInspector, persistentStore, customTabsInternalClient); @@ -427,7 +469,14 @@ public void deliverResultFromCache_forwardsCachedResultFromBrowserSwitchPersiste when(activity.getApplicationContext()).thenReturn(applicationContext); JSONObject requestMetadata = new JSONObject(); - BrowserSwitchRequest request = new BrowserSwitchRequest(123, browserSwitchDestinationUrl, requestMetadata, "return-url-scheme", false); + BrowserSwitchRequest request = new BrowserSwitchRequest( + 123, + browserSwitchDestinationUrl, + requestMetadata, + "return-url-scheme", + appLinkUri, + false + ); BrowserSwitchResult cachedResult = new BrowserSwitchResult(BrowserSwitchStatus.SUCCESS, request, Uri.parse("example://success/url")); when(persistentStore.getActiveResult(same(applicationContext))).thenReturn(cachedResult); @@ -443,8 +492,14 @@ public void parseResult_whenActiveRequestMatchesRequestCodeAndDeepLinkResultURLS BrowserSwitchClient sut = new BrowserSwitchClient(browserSwitchInspector, persistentStore, 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", + appLinkUri, + false + ); when(persistentStore.getActiveRequest(same(applicationContext))).thenReturn(request); Uri deepLinkUrl = Uri.parse("fake-url-scheme://success"); @@ -461,8 +516,14 @@ public void parseResult_whenActiveRequestMatchesRequestCodeAndDeepLinkResultURLS BrowserSwitchClient sut = new BrowserSwitchClient(browserSwitchInspector, persistentStore, 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", + appLinkUri, + false + ); when(persistentStore.getActiveRequest(same(applicationContext))).thenReturn(request); Uri deepLinkUrl = Uri.parse("a-different-url-scheme://success"); @@ -477,8 +538,14 @@ public void parseResult_whenActiveRequestDoesntMatchRequestCode_returnsNull() { BrowserSwitchClient sut = new BrowserSwitchClient(browserSwitchInspector, persistentStore, customTabsInternalClient); JSONObject requestMetadata = new JSONObject(); - BrowserSwitchRequest request = - new BrowserSwitchRequest(456, browserSwitchDestinationUrl, requestMetadata, "fake-url-scheme", false); + BrowserSwitchRequest request = new BrowserSwitchRequest( + 456, + browserSwitchDestinationUrl, + requestMetadata, + "fake-url-scheme", + appLinkUri, + false + ); when(persistentStore.getActiveRequest(same(applicationContext))).thenReturn(request); Uri deepLinkUrl = Uri.parse("fake-url-scheme://success"); diff --git a/browser-switch/src/test/java/com/braintreepayments/api/BrowserSwitchRequestUnitTest.java b/browser-switch/src/test/java/com/braintreepayments/api/BrowserSwitchRequestUnitTest.java index 6adf5887..ae2f7d7d 100644 --- a/browser-switch/src/test/java/com/braintreepayments/api/BrowserSwitchRequestUnitTest.java +++ b/browser-switch/src/test/java/com/braintreepayments/api/BrowserSwitchRequestUnitTest.java @@ -29,7 +29,7 @@ public void fromJson_withoutShouldNotifyProperty_defaultsShouldNotifyToTrue() th } @Test - public void fromJson_withoutMetadata() throws JSONException { + public void fromJson_withoutMetadata_or_appLinkUri() throws JSONException { String json = "{\n" + " \"requestCode\": 123,\n" + " \"url\": \"https://example.com\",\n" + @@ -41,6 +41,7 @@ public void fromJson_withoutMetadata() throws JSONException { assertEquals(sut.getRequestCode(), 123); assertEquals(sut.getUrl().toString(), "https://example.com"); assertNull(sut.getMetadata()); + assertNull(sut.getAppLinkUri()); assertTrue(sut.getShouldNotifyCancellation()); assertTrue(sut.matchesDeepLinkUrlScheme(Uri.parse("my-return-url-scheme://test"))); @@ -48,7 +49,7 @@ public void fromJson_withoutMetadata() throws JSONException { } @Test - public void toJson_withoutMetadata() throws JSONException { + public void toJson_withoutMetadata_or_appLinkUri() throws JSONException { String json = "{\n" + " \"requestCode\": 123,\n" + " \"url\": \"https://example.com\",\n" + @@ -61,6 +62,7 @@ public void toJson_withoutMetadata() throws JSONException { assertEquals(restored.getRequestCode(), original.getRequestCode()); assertEquals(restored.getUrl(), original.getUrl()); assertNull(restored.getMetadata()); + assertNull(restored.getAppLinkUri()); assertFalse(restored.getShouldNotifyCancellation()); assertTrue(restored.matchesDeepLinkUrlScheme(Uri.parse("my-return-url-scheme://test"))); @@ -68,12 +70,13 @@ public void toJson_withoutMetadata() throws JSONException { } @Test - public void fromJson_withMetadata() throws JSONException { + public void fromJson_withMetadata_and_appLinkUri() throws JSONException { String json = "{\n" + " \"requestCode\": 123,\n" + " \"url\": \"https://example.com\",\n" + " \"returnUrlScheme\": \"my-return-url-scheme\",\n" + " \"shouldNotify\": true,\n" + + " \"appLinkUri\": \"https://example.com\",\n" + " \"metadata\": {\n" + " \"testKey\": \"testValue\"" + " }" + @@ -82,6 +85,7 @@ public void fromJson_withMetadata() throws JSONException { assertEquals(sut.getRequestCode(), 123); assertEquals(sut.getUrl().toString(), "https://example.com"); + assertEquals(sut.getAppLinkUri().toString(), "https://example.com"); assertTrue(sut.getShouldNotifyCancellation()); JSONAssert.assertEquals(sut.getMetadata(), new JSONObject().put("testKey", "testValue"), true); @@ -90,12 +94,13 @@ public void fromJson_withMetadata() throws JSONException { } @Test - public void toJson_withMetadata() throws JSONException { + public void toJson_withMetadata_and_appLinkUri() throws JSONException { String json = "{\n" + " \"requestCode\": 123,\n" + " \"url\": \"https://example.com\",\n" + " \"returnUrlScheme\": \"my-return-url-scheme\",\n" + " \"shouldNotify\": false,\n" + + " \"appLinkUri\": \"https://example.com\",\n" + " \"metadata\": {\n" + " \"testKey\": \"testValue\"" + " }" + @@ -105,6 +110,7 @@ public void toJson_withMetadata() throws JSONException { assertEquals(restored.getRequestCode(), original.getRequestCode()); assertEquals(restored.getUrl(), original.getUrl()); + assertEquals("https://example.com", restored.getAppLinkUri().toString()); assertFalse(restored.getShouldNotifyCancellation()); JSONAssert.assertEquals(restored.getMetadata(), original.getMetadata(), true); diff --git a/browser-switch/src/test/java/com/braintreepayments/api/BrowserSwitchResultUnitTest.java b/browser-switch/src/test/java/com/braintreepayments/api/BrowserSwitchResultUnitTest.java index cf226f27..6f02799d 100644 --- a/browser-switch/src/test/java/com/braintreepayments/api/BrowserSwitchResultUnitTest.java +++ b/browser-switch/src/test/java/com/braintreepayments/api/BrowserSwitchResultUnitTest.java @@ -17,11 +17,12 @@ public class BrowserSwitchResultUnitTest { @Test public void toJSON_serializesResult() throws JSONException { Uri requestUrl = Uri.parse("https://www.example.com"); + Uri appLinkUri = Uri.parse("https://www.example.com"); String returnUrlScheme = "example.return.url.scheme"; JSONObject requestMetadata = new JSONObject() - .put("sample", "value"); + .put("sample", "value"); BrowserSwitchRequest request = - new BrowserSwitchRequest(123, requestUrl, requestMetadata, returnUrlScheme, true); + new BrowserSwitchRequest(123, requestUrl, requestMetadata, returnUrlScheme, appLinkUri, true); Uri deepLinkUrl = Uri.parse("example.return.url.scheme://success/ok"); BrowserSwitchResult sut = new BrowserSwitchResult(BrowserSwitchStatus.SUCCESS, request, deepLinkUrl); From a0ac5d3187dad175a699bea1327ee461c31db347 Mon Sep 17 00:00:00 2001 From: Tim Chow Date: Mon, 6 May 2024 15:26:54 -0500 Subject: [PATCH 2/6] Update assertCanPerformBrowserSwitch in BrowserSwitchClient for app link support (#93) --- .../braintreepayments/api/BrowserSwitchClient.java | 12 ++++++++---- browser-switch/src/main/res/values/strings.xml | 2 +- .../api/BrowserSwitchClientUnitTest.java | 5 +++-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/browser-switch/src/main/java/com/braintreepayments/api/BrowserSwitchClient.java b/browser-switch/src/main/java/com/braintreepayments/api/BrowserSwitchClient.java index 5e348cd1..a7389e95 100644 --- a/browser-switch/src/main/java/com/braintreepayments/api/BrowserSwitchClient.java +++ b/browser-switch/src/main/java/com/braintreepayments/api/BrowserSwitchClient.java @@ -77,7 +77,10 @@ public void start(@NonNull FragmentActivity activity, @NonNull BrowserSwitchOpti } } - void assertCanPerformBrowserSwitch(FragmentActivity activity, BrowserSwitchOptions browserSwitchOptions) throws BrowserSwitchException { + void assertCanPerformBrowserSwitch( + FragmentActivity activity, + BrowserSwitchOptions browserSwitchOptions + ) throws BrowserSwitchException { Context appContext = activity.getApplicationContext(); int requestCode = browserSwitchOptions.getRequestCode(); @@ -87,9 +90,10 @@ void assertCanPerformBrowserSwitch(FragmentActivity activity, BrowserSwitchOptio 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); } diff --git a/browser-switch/src/main/res/values/strings.xml b/browser-switch/src/main/res/values/strings.xml index 3dc5fb75..217ede1a 100644 --- a/browser-switch/src/main/res/values/strings.xml +++ b/browser-switch/src/main/res/values/strings.xml @@ -1,7 +1,7 @@ Request code cannot be Integer.MIN_VALUE - A returnUrlScheme is required. + An appLinkUri or returnUrlScheme is required. No installed activities can open this URL: %1$s 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. diff --git a/browser-switch/src/test/java/com/braintreepayments/api/BrowserSwitchClientUnitTest.java b/browser-switch/src/test/java/com/braintreepayments/api/BrowserSwitchClientUnitTest.java index 91d8b5a2..abdbf12a 100644 --- a/browser-switch/src/test/java/com/braintreepayments/api/BrowserSwitchClientUnitTest.java +++ b/browser-switch/src/test/java/com/braintreepayments/api/BrowserSwitchClientUnitTest.java @@ -228,7 +228,7 @@ public void start_whenDeviceIsNotConfiguredForDeepLinking_throwsError() { } @Test - public void start_whenNoReturnUrlSchemeSet_throwsError() { + public void start_whenNoAppLinkUriOrReturnUrlSchemeSet_throwsError() { when(browserSwitchInspector.deviceHasBrowser(applicationContext)).thenReturn(true); when(browserSwitchInspector.isDeviceConfiguredForDeepLinking(applicationContext, "return-url-scheme")).thenReturn(true); @@ -238,13 +238,14 @@ public void start_whenNoReturnUrlSchemeSet_throwsError() { BrowserSwitchOptions options = new BrowserSwitchOptions() .requestCode(123) .returnUrlScheme(null) + .appLinkUri(null) .url(browserSwitchDestinationUrl) .metadata(metadata); try { sut.start(activity, options); fail("should fail"); } catch (BrowserSwitchException e) { - assertEquals("A returnUrlScheme is required.", e.getMessage()); + assertEquals("An appLinkUri or returnUrlScheme is required.", e.getMessage()); } } From f9969a6189d39e2ffc26e34cc206a64954485fd0 Mon Sep 17 00:00:00 2001 From: Tim Chow Date: Tue, 7 May 2024 15:43:47 -0500 Subject: [PATCH 3/6] Add matchesAppLinkUri to BrowserSwitchRequest for App Link support (#94) --- .../api/BrowserSwitchClient.java | 13 +-- .../api/BrowserSwitchRequest.java | 5 +- .../api/BrowserSwitchClientUnitTest.java | 91 ++++++++++++++++++- .../api/BrowserSwitchRequestUnitTest.java | 48 ++++++++++ 4 files changed, 147 insertions(+), 10 deletions(-) diff --git a/browser-switch/src/main/java/com/braintreepayments/api/BrowserSwitchClient.java b/browser-switch/src/main/java/com/braintreepayments/api/BrowserSwitchClient.java index a7389e95..8c22c052 100644 --- a/browser-switch/src/main/java/com/braintreepayments/api/BrowserSwitchClient.java +++ b/browser-switch/src/main/java/com/braintreepayments/api/BrowserSwitchClient.java @@ -167,9 +167,10 @@ public BrowserSwitchResult getResult(@NonNull FragmentActivity activity) { BrowserSwitchResult result = null; - Uri deepLinkUrl = intent.getData(); - if (deepLinkUrl != null && request.matchesDeepLinkUrlScheme(deepLinkUrl)) { - result = new BrowserSwitchResult(BrowserSwitchStatus.SUCCESS, request, deepLinkUrl); + Uri linkUrl = intent.getData(); + if (linkUrl != null && + (request.matchesDeepLinkUrlScheme(linkUrl) || request.matchesAppLinkUri(linkUrl))) { + result = new BrowserSwitchResult(BrowserSwitchStatus.SUCCESS, request, linkUrl); } else if (request.getShouldNotifyCancellation()) { result = new BrowserSwitchResult(BrowserSwitchStatus.CANCELED, request); } @@ -196,9 +197,9 @@ public BrowserSwitchResult parseResult(@NonNull Context context, int requestCode BrowserSwitchRequest request = persistentStore.getActiveRequest(context.getApplicationContext()); if (request != null && request.getRequestCode() == requestCode) { - Uri deepLinkUrl = intent.getData(); - if (request.matchesDeepLinkUrlScheme(deepLinkUrl)) { - result = new BrowserSwitchResult(BrowserSwitchStatus.SUCCESS, request, deepLinkUrl); + Uri linkUrl = intent.getData(); + if (request.matchesDeepLinkUrlScheme(linkUrl) || request.matchesAppLinkUri(linkUrl)) { + result = new BrowserSwitchResult(BrowserSwitchStatus.SUCCESS, request, linkUrl); } } } diff --git a/browser-switch/src/main/java/com/braintreepayments/api/BrowserSwitchRequest.java b/browser-switch/src/main/java/com/braintreepayments/api/BrowserSwitchRequest.java index 1d492324..478cd9c2 100644 --- a/browser-switch/src/main/java/com/braintreepayments/api/BrowserSwitchRequest.java +++ b/browser-switch/src/main/java/com/braintreepayments/api/BrowserSwitchRequest.java @@ -11,7 +11,6 @@ class BrowserSwitchRequest { - private final Uri url; private final int requestCode; private final JSONObject metadata; @@ -105,4 +104,8 @@ String toJson() throws JSONException { boolean matchesDeepLinkUrlScheme(@NonNull Uri url) { return url.getScheme() != null && url.getScheme().equalsIgnoreCase(returnUrlScheme); } + + boolean matchesAppLinkUri(@NonNull Uri uri) { + return appLinkUri != null && uri.toString().equals(appLinkUri.toString()); + } } diff --git a/browser-switch/src/test/java/com/braintreepayments/api/BrowserSwitchClientUnitTest.java b/browser-switch/src/test/java/com/braintreepayments/api/BrowserSwitchClientUnitTest.java index abdbf12a..56638acf 100644 --- a/browser-switch/src/test/java/com/braintreepayments/api/BrowserSwitchClientUnitTest.java +++ b/browser-switch/src/test/java/com/braintreepayments/api/BrowserSwitchClientUnitTest.java @@ -249,6 +249,47 @@ public void start_whenNoAppLinkUriOrReturnUrlSchemeSet_throwsError() { } } + @Test + public void getResult_whenAppLinkMatches_successReturnedWithAppLink() { + BrowserSwitchClient sut = new BrowserSwitchClient(browserSwitchInspector, persistentStore, customTabsInternalClient); + Intent deepLinkIntent = new Intent().setData(appLinkUri); + when(activity.getIntent()).thenReturn(deepLinkIntent); + BrowserSwitchRequest request = new BrowserSwitchRequest( + 123, + browserSwitchDestinationUrl, + new JSONObject(), + null, + appLinkUri, + true + ); + when(persistentStore.getActiveRequest(applicationContext)).thenReturn(request); + + BrowserSwitchResult result = sut.getResult(activity); + + assertEquals(BrowserSwitchStatus.SUCCESS, result.getStatus()); + assertEquals(appLinkUri, result.getDeepLinkUrl()); + } + + @Test + public void getResult_whenAppLinkDoesNotMatch_canceledIsReturned() { + BrowserSwitchClient sut = new BrowserSwitchClient(browserSwitchInspector, persistentStore, customTabsInternalClient); + Intent deepLinkIntent = new Intent().setData(appLinkUri); + when(activity.getIntent()).thenReturn(deepLinkIntent); + BrowserSwitchRequest request = new BrowserSwitchRequest( + 123, + browserSwitchDestinationUrl, + new JSONObject(), + null, + Uri.parse("https://different-example.com"), + true + ); + when(persistentStore.getActiveRequest(applicationContext)).thenReturn(request); + + BrowserSwitchResult result = sut.getResult(activity); + + assertEquals(BrowserSwitchStatus.CANCELED, result.getStatus()); + } + @Test public void deliverResult_whenDeepLinkUrlExistsAndReturnUrlSchemeMatchesAndNoPendingRequest_returnsNull() { when(activity.getApplicationContext()).thenReturn(applicationContext); @@ -498,7 +539,7 @@ public void parseResult_whenActiveRequestMatchesRequestCodeAndDeepLinkResultURLS browserSwitchDestinationUrl, requestMetadata, "fake-url-scheme", - appLinkUri, + null, false ); when(persistentStore.getActiveRequest(same(applicationContext))).thenReturn(request); @@ -522,7 +563,7 @@ public void parseResult_whenActiveRequestMatchesRequestCodeAndDeepLinkResultURLS browserSwitchDestinationUrl, requestMetadata, "fake-url-scheme", - appLinkUri, + null, false ); when(persistentStore.getActiveRequest(same(applicationContext))).thenReturn(request); @@ -534,6 +575,50 @@ public void parseResult_whenActiveRequestMatchesRequestCodeAndDeepLinkResultURLS assertNull(browserSwitchResult); } + @Test + public void parseResult_whenActiveRequestMatchesRequestCodeAndAppLinkUri_returnsBrowserSwitchSuccessResult() { + String appLinkUrl = "https://example.com"; + BrowserSwitchClient sut = new BrowserSwitchClient(browserSwitchInspector, persistentStore, customTabsInternalClient); + BrowserSwitchRequest request = new BrowserSwitchRequest( + 123, + browserSwitchDestinationUrl, + new JSONObject(), + null, + Uri.parse(appLinkUrl), + false + ); + when(persistentStore.getActiveRequest(same(applicationContext))).thenReturn(request); + + Uri appLinkUri = Uri.parse(appLinkUrl); + Intent intent = new Intent(Intent.ACTION_VIEW, appLinkUri); + BrowserSwitchResult browserSwitchResult = sut.parseResult(applicationContext, 123, intent); + + assertNotNull(browserSwitchResult); + assertEquals(BrowserSwitchStatus.SUCCESS, browserSwitchResult.getStatus()); + assertEquals(appLinkUri, browserSwitchResult.getDeepLinkUrl()); + } + + @Test + public void parseResult_whenActiveRequestMatchesRequestCodeAndAppLinkDoesntMatch_returnsNull() { + BrowserSwitchClient sut = new BrowserSwitchClient(browserSwitchInspector, persistentStore, customTabsInternalClient); + + BrowserSwitchRequest request = new BrowserSwitchRequest( + 123, + browserSwitchDestinationUrl, + new JSONObject(), + null, + Uri.parse("https://example.com"), + false + ); + when(persistentStore.getActiveRequest(same(applicationContext))).thenReturn(request); + + Uri appLinkUri = Uri.parse("https://different-example.com"); + Intent intent = new Intent(Intent.ACTION_VIEW, appLinkUri); + BrowserSwitchResult browserSwitchResult = sut.parseResult(applicationContext, 123, intent); + + assertNull(browserSwitchResult); + } + @Test public void parseResult_whenActiveRequestDoesntMatchRequestCode_returnsNull() { BrowserSwitchClient sut = new BrowserSwitchClient(browserSwitchInspector, persistentStore, customTabsInternalClient); @@ -544,7 +629,7 @@ public void parseResult_whenActiveRequestDoesntMatchRequestCode_returnsNull() { browserSwitchDestinationUrl, requestMetadata, "fake-url-scheme", - appLinkUri, + null, false ); when(persistentStore.getActiveRequest(same(applicationContext))).thenReturn(request); diff --git a/browser-switch/src/test/java/com/braintreepayments/api/BrowserSwitchRequestUnitTest.java b/browser-switch/src/test/java/com/braintreepayments/api/BrowserSwitchRequestUnitTest.java index ae2f7d7d..732598c2 100644 --- a/browser-switch/src/test/java/com/braintreepayments/api/BrowserSwitchRequestUnitTest.java +++ b/browser-switch/src/test/java/com/braintreepayments/api/BrowserSwitchRequestUnitTest.java @@ -146,4 +146,52 @@ public void matchesDeepLinkUrlScheme_whenDifferentScheme_returnsFalse() throws J BrowserSwitchRequest request = BrowserSwitchRequest.fromJson(json); assertFalse(request.matchesDeepLinkUrlScheme(Uri.parse("not-my-return-url-scheme://example.com"))); } + + @Test + public void matchesAppLinkUri_whenTheSame_returnsTrue() throws JSONException { + String json = "{\n" + + " \"requestCode\": 123,\n" + + " \"url\": \"https://example.com\",\n" + + " \"returnUrlScheme\": \"my-return-url-scheme\",\n" + + " \"appLinkUri\": \"https://example.com\",\n" + + " \"shouldNotify\": false,\n" + + " \"metadata\": {\n" + + " \"testKey\": \"testValue\"" + + " }" + + "}"; + BrowserSwitchRequest request = BrowserSwitchRequest.fromJson(json); + assertTrue(request.matchesAppLinkUri(Uri.parse("https://example.com"))); + } + + @Test + public void matchesAppLinkUri_whenDifferent_returnsFalse() throws JSONException { + String json = "{\n" + + " \"requestCode\": 123,\n" + + " \"url\": \"https://example.com\",\n" + + " \"returnUrlScheme\": \"my-return-url-scheme\",\n" + + " \"appLinkUri\": \"https://example.com\",\n" + + " \"shouldNotify\": false,\n" + + " \"metadata\": {\n" + + " \"testKey\": \"testValue\"" + + " }" + + "}"; + BrowserSwitchRequest request = BrowserSwitchRequest.fromJson(json); + assertFalse(request.matchesAppLinkUri(Uri.parse("https://another-example.com"))); + } + + @Test + public void matchesAppLinkUri_whenNull_returnsFalse() throws JSONException { + String json = "{\n" + + " \"requestCode\": 123,\n" + + " \"url\": \"https://example.com\",\n" + + " \"returnUrlScheme\": \"my-return-url-scheme\",\n" + + " \"shouldNotify\": false,\n" + + " \"metadata\": {\n" + + " \"testKey\": \"testValue\"" + + " }" + + "}"; + BrowserSwitchRequest request = BrowserSwitchRequest.fromJson(json); + assertNull(request.getAppLinkUri()); + assertFalse(request.matchesAppLinkUri(Uri.parse("https://example.com"))); + } } From b78211a5129b129eda97cc411dffc08a3a7b7337 Mon Sep 17 00:00:00 2001 From: Tim Chow Date: Thu, 9 May 2024 10:31:34 -0500 Subject: [PATCH 4/6] Add App Link toggle to DemoFragment (#96) --- .../api/demo/DemoFragment.java | 44 +++++++++++++++---- demo/src/main/res/layout/demo_fragment.xml | 11 +++++ demo/src/main/res/values/arrays.xml | 9 ++++ demo/src/main/res/values/strings.xml | 1 + 4 files changed, 56 insertions(+), 9 deletions(-) create mode 100644 demo/src/main/res/values/arrays.xml diff --git a/demo/src/main/java/com/braintreepayments/api/demo/DemoFragment.java b/demo/src/main/java/com/braintreepayments/api/demo/DemoFragment.java index cbfb1d44..2857e1e3 100644 --- a/demo/src/main/java/com/braintreepayments/api/demo/DemoFragment.java +++ b/demo/src/main/java/com/braintreepayments/api/demo/DemoFragment.java @@ -5,7 +5,10 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; import android.widget.Button; +import android.widget.Spinner; import android.widget.TextView; import androidx.annotation.IdRes; @@ -26,6 +29,8 @@ public class DemoFragment extends Fragment implements View.OnClickListener { private static final String TEST_KEY = "testKey"; private static final String TEST_VALUE = "testValue"; + private Spinner mLinkSpinner; + private TextView mBrowserSwitchStatusTextView; private TextView mSelectedColorTextView; private TextView mMetadataTextView; @@ -35,6 +40,15 @@ public class DemoFragment extends Fragment implements View.OnClickListener { public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.demo_fragment, container, false); + mLinkSpinner = view.findViewById(R.id.link_spinner); + ArrayAdapter adapter = ArrayAdapter.createFromResource( + requireContext(), + R.array.navigation_links, + R.layout.support_simple_spinner_dropdown_item + ); + adapter.setDropDownViewResource(R.layout.support_simple_spinner_dropdown_item); + mLinkSpinner.setAdapter(adapter); + Button startBrowserSwitchButton = view.findViewById(R.id.browser_switch); startBrowserSwitchButton.setOnClickListener(this); @@ -61,13 +75,21 @@ public void onClick(View v) { } private void startBrowserSwitch(@Nullable JSONObject metadata) { - Uri url = buildBrowserSwitchUrl(); + boolean isAppLink = mLinkSpinner.getSelectedItem().equals("App Link"); + Uri url = buildBrowserSwitchUrl(isAppLink); BrowserSwitchOptions browserSwitchOptions = new BrowserSwitchOptions() - .metadata(metadata) - .requestCode(1) - .url(url) - .launchAsNewTask(true) - .returnUrlScheme(getReturnUrlScheme()); + .metadata(metadata) + .requestCode(1) + .url(url) + .launchAsNewTask(true); + + if (isAppLink) { + browserSwitchOptions.appLinkUri( + Uri.parse("https://mobile-sdk-demo-site-838cead5d3ab.herokuapp.com") + ); + } else { + browserSwitchOptions.returnUrlScheme(getReturnUrlScheme()); + } try { getDemoActivity().startBrowserSwitch(browserSwitchOptions); @@ -79,10 +101,14 @@ private void startBrowserSwitch(@Nullable JSONObject metadata) { } } - private Uri buildBrowserSwitchUrl() { + private Uri buildBrowserSwitchUrl(boolean isAppLink) { String url = "https://braintree.github.io/popup-bridge-example/" + - "this_launches_in_popup.html?popupBridgeReturnUrlPrefix=" + - getReturnUrlScheme() + "://"; + "this_launches_in_popup.html?"; + if (isAppLink) { + url += "isAppLink=true"; + } else { + url += "popupBridgeReturnUrlPrefix=" + getReturnUrlScheme() + "://"; + } return Uri.parse(url); } diff --git a/demo/src/main/res/layout/demo_fragment.xml b/demo/src/main/res/layout/demo_fragment.xml index 0d475d33..da72c7f1 100644 --- a/demo/src/main/res/layout/demo_fragment.xml +++ b/demo/src/main/res/layout/demo_fragment.xml @@ -8,6 +8,17 @@ tools:context=".DemoFragment" > + + + +