Skip to content

Commit 4700f64

Browse files
author
Martin Dinh
committed
Merge branch '49-add-handler-for-redirect-scheme' into 'master'
Resolve "Add handler for redirect scheme" Closes #49 See merge request pace/mobile/android/pace-cloud-sdk!44
2 parents c57145d + c5c8e7e commit 4700f64

File tree

9 files changed

+77
-53
lines changed

9 files changed

+77
-53
lines changed

README.md

Lines changed: 19 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -53,18 +53,6 @@ dependencies {
5353
}
5454
```
5555

56-
Because the PACE Cloud SDK uses [AppAuth for Android](https://github.com/openid/AppAuth-Android) for the *IDKit*, the AppAuth redirect scheme must be registered in your app's `build.gradle` file:
57-
```groovy
58-
android {
59-
...
60-
defaultConfig {
61-
...
62-
manifestPlaceholders = ['appAuthRedirectScheme': 'YOUR_REDIRECT_URI_SCHEME_OR_EMPTY']
63-
}
64-
...
65-
}
66-
```
67-
6856
## Setup
6957
The `PACECloudSDK` needs to be setup before any of its `Kits` can be used. Therefore you *must* call `PACECloudSDK.setup(context: Context, configuration: Configuration)`. The best way to do this is inside your `Application` class. It will automatically authorize your application with the provided api key.
7058

@@ -79,13 +67,28 @@ clientAppName: String
7967
clientAppVersion: String
8068
clientAppBuild: String
8169
apiKey: String
82-
clientId: String? // Default: null
8370
authenticationMode: AuthenticationMode // Default: AuthenticationMode.WEB
8471
environment: Environment // Default: Environment.PRODUCTION
8572
extensions: List<String> // Default: emptyList()
8673
locationAccuracy: Int? // Default: null
8774
```
8875

76+
PACE Cloud SDK uses [AppAuth for Android](https://github.com/openid/AppAuth-Android) for the native authentication in *IDKit*, which needs `appAuthRedirectScheme` manifest placeholder to be set. PACE Cloud SDK requires `pace_redirect_scheme` for [Deep Linking](#deep-linking) to be set. Both these manifest placeholder must be configured in your app's `build.gradle` file. In case you won't be using native login, you can set an empty string for `appAuthRedirectScheme`.
77+
78+
For the `pace_redirect_scheme` we recommend to use the following pattern to prevent collisions with other apps that might be using PACE Cloud SDK: `pace.$UUID`, where `$UUID` can be any UUID of your choice:
79+
```groovy
80+
android {
81+
...
82+
defaultConfig {
83+
...
84+
manifestPlaceholders = [
85+
'appAuthRedirectScheme': 'YOUR_REDIRECT_URI_SCHEME_OR_EMPTY', // e.g. reverse domain name notation: cloud.pace.app
86+
'pace_redirect_scheme': 'YOUR_REDIRECT_SCHEME_OR_EMPTY'] // e.g. pace.ad50262a-9c88-4a5f-bc55-00dc31b81e5a
87+
}
88+
...
89+
}
90+
```
91+
8992
## Migration
9093
### From 2.x.x to 3.x.x
9194
In `3.0.0` we've introduced a universal setup method: `PACECloudSDK.setup(context: Context, configuration: Configuration)` and removed the setup for `AppKit` and `POIKit`.
@@ -102,7 +105,6 @@ val config = OIDConfiguration(
102105
authorizationEndpoint,
103106
tokenEndpoint,
104107
userInfoEndpoint, // optional
105-
clientId,
106108
clientSecret, // optional
107109
scopes, // optional
108110
redirectUri,
@@ -489,32 +491,12 @@ AppKit.requestLocalApps { app ->
489491
}
490492
```
491493
**Note**: For a more detailed example, where the apps are displayed in a `RecyclerView`, see the `PACECloudSDK` example app.
492-
494+
493495
### Deep Linking
494496
Some of our services (e.g. `PayPal`) do not open the URL in the WebView, but in a Chrome Custom Tab within the app, due to security reasons. After completion of the process the user is redirected back to the WebView via deep linking. In order to set the redirect URL correctly and to ensure that the client app intercepts the deep link, the following requirements must be met:
495497

496-
* Set `clientId` in *PACECloudSDK's* configuration during the [setup](#setup), because it is needed for the redirect URL
497-
* Specify the [AppActivity](#appactivity) as deep link intent filter in your app manifest. **`pace.${clientId}` (same `clientId` as passed in the configuration) must be passed to `android:scheme`:**
498-
* If the scheme is not set, the *AppKit* calls the `onCustomSchemeError(context: Context?, scheme: String)` callback
499-
500-
```xml
501-
<activity
502-
android:name="cloud.pace.sdk.appkit.app.AppActivity"
503-
android:launchMode="singleTop"
504-
android:screenOrientation="portrait"
505-
android:theme="@style/AppKitTheme">
506-
<intent-filter>
507-
<action android:name="android.intent.action.VIEW" />
508-
509-
<category android:name="android.intent.category.DEFAULT" />
510-
<category android:name="android.intent.category.BROWSABLE" />
511-
512-
<data
513-
android:host="redirect"
514-
android:scheme="pace.$clientId" />
515-
</intent-filter>
516-
</activity>
517-
```
498+
* Specify the `pace_redirect_scheme` as manifest placeholder in your app's `build.gradle` file (see [setup](#setup))
499+
* If the scheme is empty, the *AppKit* calls the `onCustomSchemeError(context: Context?, scheme: String)` callback
518500

519501
### Native login
520502
If the client app uses its own login and wants to pass an access token to the apps, follow these steps:

app/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ android {
1515

1616
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
1717

18-
manifestPlaceholders = ['appAuthRedirectScheme': 'pace']
18+
manifestPlaceholders = ['appAuthRedirectScheme': 'pace', 'pace_redirect_scheme': '']
1919
}
2020

2121
buildTypes {

library/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ android {
1313
targetSdkVersion rootProject.ext.target_sdk_version
1414
versionCode rootProject.ext.build_number
1515
versionName rootProject.ext.version_name
16+
manifestPlaceholders = [pace_redirect_scheme: "\${pace_redirect_scheme}"]
1617

1718
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
1819

library/src/main/AndroidManifest.xml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,23 @@
1111
android:name=".appkit.app.AppActivity"
1212
android:launchMode="singleTop"
1313
android:screenOrientation="portrait"
14-
android:theme="@style/AppKitTheme" />
14+
android:theme="@style/AppKitTheme">
15+
<intent-filter>
16+
<action android:name="android.intent.action.VIEW" />
17+
18+
<category android:name="android.intent.category.DEFAULT" />
19+
<category android:name="android.intent.category.BROWSABLE" />
20+
21+
<data
22+
android:host="redirect"
23+
android:scheme="${pace_redirect_scheme}" />
24+
</intent-filter>
25+
</activity>
1526

1627
<receiver android:name=".appkit.geofences.GeofenceBroadcastReceiver" />
28+
<meta-data
29+
android:name="pace_redirect_scheme"
30+
android:value="${pace_redirect_scheme}" />
1731
</application>
1832

1933
</manifest>

library/src/main/java/cloud/pace/sdk/appkit/AppKit.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ object AppKit : CloudSDKKoinComponent {
4444
"${config.clientAppName}/${config.clientAppVersion}_${config.clientAppBuild}",
4545
"(${DeviceUtils.getDeviceName()} Android/${DeviceUtils.getAndroidVersion()})",
4646
"PWA-SDK/${BuildConfig.VERSION_NAME}",
47-
if (config.clientId != null) "(clientid:${config.clientId};)" else "",
4847
if (theme == Theme.LIGHT) "PWASDK-Theme/Light" else "PWASDK-Theme/Dark",
4948
"IdentityManagement/${config.authenticationMode.value}",
5049
config.extensions.joinToString(" ")

library/src/main/java/cloud/pace/sdk/appkit/app/webview/AppWebView.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,12 @@ class AppWebView(context: Context, attributeSet: AttributeSet) : RelativeLayout(
111111
}
112112
}
113113

114+
private val appInterceptableLinkObserver = Observer<Event<AppWebViewModel.AppInterceptableLinkResponse>> {
115+
it.getContentIfNotHandled()?.let { appInterceptableLinkResponse ->
116+
sendMessageCallback(gson.toJson(appInterceptableLinkResponse))
117+
}
118+
}
119+
114120
init {
115121
addView(View.inflate(context, R.layout.app_web_view, null))
116122

@@ -137,6 +143,7 @@ class AppWebView(context: Context, attributeSet: AttributeSet) : RelativeLayout(
137143
webView.addJavascriptInterface(GetSecureDataInterface(), "pace_getSecureData")
138144
webView.addJavascriptInterface(DisableInterface(), "pace_disable")
139145
webView.addJavascriptInterface(OpenURLInNewTabInterface(), "pace_openURLInNewTab")
146+
webView.addJavascriptInterface(GetAppInterceptableLinkInterface(), "pace_getAppInterceptableLink")
140147

141148
failureView.setButtonClickListener {
142149
webView.reload()
@@ -179,6 +186,7 @@ class AppWebView(context: Context, attributeSet: AttributeSet) : RelativeLayout(
179186
webViewModel.statusCode.observe(lifecycleOwner, statusCodeObserver)
180187
webViewModel.totpResponse.observe(lifecycleOwner, totpResponseObserver)
181188
webViewModel.secureData.observe(lifecycleOwner, secureDataObserver)
189+
webViewModel.appInterceptableLink.observe(lifecycleOwner, appInterceptableLinkObserver)
182190
}
183191

184192
private fun handleBack() {
@@ -276,4 +284,11 @@ class AppWebView(context: Context, attributeSet: AttributeSet) : RelativeLayout(
276284
onMainThread { webViewModel.handleOpenURLInNewTab(message) }
277285
}
278286
}
287+
288+
inner class GetAppInterceptableLinkInterface {
289+
@JavascriptInterface
290+
fun postMessage(message: String) {
291+
onMainThread { webViewModel.handleGetAppInterceptableLink(message) }
292+
}
293+
}
279294
}

library/src/main/java/cloud/pace/sdk/appkit/app/webview/AppWebViewModel.kt

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package cloud.pace.sdk.appkit.app.webview
22

33
import android.content.Context
4-
import android.content.Intent
54
import android.content.pm.PackageManager
65
import android.graphics.BitmapFactory
76
import android.location.Location
@@ -11,7 +10,6 @@ import androidx.annotation.StringRes
1110
import androidx.lifecycle.LiveData
1211
import androidx.lifecycle.MutableLiveData
1312
import androidx.lifecycle.ViewModel
14-
import cloud.pace.sdk.PACECloudSDK
1513
import cloud.pace.sdk.R
1614
import cloud.pace.sdk.appkit.communication.AppEventManager
1715
import cloud.pace.sdk.appkit.communication.AppModel
@@ -28,7 +26,6 @@ import com.google.gson.JsonSyntaxException
2826
import dev.turingcomplete.kotlinonetimepassword.HmacAlgorithm
2927
import dev.turingcomplete.kotlinonetimepassword.TimeBasedOneTimePasswordConfig
3028
import dev.turingcomplete.kotlinonetimepassword.TimeBasedOneTimePasswordGenerator
31-
import kotlinx.android.synthetic.main.app_web_view.view.*
3229
import org.apache.commons.codec.binary.Base32
3330
import java.util.*
3431
import java.util.concurrent.TimeUnit
@@ -45,6 +42,7 @@ abstract class AppWebViewModel : ViewModel(), AppWebViewClient.WebClientCallback
4542
abstract val statusCode: LiveData<Event<StatusCodeResponse>>
4643
abstract val totpResponse: LiveData<Event<TOTPResponse>>
4744
abstract val secureData: LiveData<Event<Map<String, String>>>
45+
abstract val appInterceptableLink: LiveData<Event<AppInterceptableLinkResponse>>
4846

4947
abstract fun init(url: String)
5048
abstract fun handleInvalidToken(message: String)
@@ -58,6 +56,7 @@ abstract class AppWebViewModel : ViewModel(), AppWebViewClient.WebClientCallback
5856
abstract fun handleGetSecureData(message: String)
5957
abstract fun handleDisable(message: String)
6058
abstract fun handleOpenURLInNewTab(message: String)
59+
abstract fun handleGetAppInterceptableLink(message: String)
6160

6261
class VerifyLocationRequest(val lat: Double, val lon: Double, val threshold: Double)
6362
class BiometricRequest(@StringRes val title: Int, val onSuccess: () -> Unit, val onFailure: (errorCode: Int, errString: CharSequence) -> Unit)
@@ -68,6 +67,7 @@ abstract class AppWebViewModel : ViewModel(), AppWebViewClient.WebClientCallback
6867
class DisableRequest(val until: Long)
6968
class OpenURLInNewTabRequest(val url: String, val cancelUrl: String)
7069
class TOTPResponse(val totp: String, val biometryMethod: String)
70+
class AppInterceptableLinkResponse(val link: String)
7171

7272
sealed class StatusCodeResponse(val statusCode: Int) {
7373
object Success : StatusCodeResponse(StatusCode.Ok.code)
@@ -106,6 +106,7 @@ class AppWebViewModelImpl(
106106
override val statusCode = MutableLiveData<Event<StatusCodeResponse>>()
107107
override val totpResponse = MutableLiveData<Event<TOTPResponse>>()
108108
override val secureData = MutableLiveData<Event<Map<String, String>>>()
109+
override val appInterceptableLink = MutableLiveData<Event<AppInterceptableLinkResponse>>()
109110

110111
private val gson = Gson()
111112

@@ -334,23 +335,38 @@ class AppWebViewModelImpl(
334335

335336
override fun handleOpenURLInNewTab(message: String) {
336337
try {
338+
val redirectScheme = getRedirectScheme()
337339
val openURLInNewTabRequest = gson.fromJson(message, OpenURLInNewTabRequest::class.java)
338-
val customScheme = "pace.${PACECloudSDK.configuration.clientId}://redirect"
339-
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(customScheme))
340-
val resolveInfo = context.packageManager?.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)?.toList()
341-
342340
url.value = Event(openURLInNewTabRequest.cancelUrl)
343341

344-
if (!resolveInfo.isNullOrEmpty()) {
342+
if (!redirectScheme.isNullOrEmpty()) {
345343
appModel.openUrlInNewTab(openURLInNewTabRequest.url)
346344
} else {
347-
appModel.onCustomSchemeError(context, customScheme)
345+
appModel.onCustomSchemeError(context, "${redirectScheme}://redirect")
348346
}
349347
} catch (e: JsonSyntaxException) {
350348
Log.e(e, "The openURLInNewTab JSON $message could not be deserialized.")
351349
}
352350
}
353351

352+
override fun handleGetAppInterceptableLink(message: String) {
353+
try {
354+
val redirectScheme = getRedirectScheme()
355+
if (!redirectScheme.isNullOrEmpty()) {
356+
appInterceptableLink.value = Event(AppInterceptableLinkResponse(redirectScheme))
357+
} else {
358+
statusCode.value = Event(StatusCodeResponse.Failure("Could not retrieve redirect scheme", StatusCode.NotFound.code))
359+
}
360+
} catch (e: Exception) {
361+
statusCode.value = Event(StatusCodeResponse.Failure("Could not retrieve redirect scheme", StatusCode.NotFound.code))
362+
}
363+
}
364+
365+
private fun getRedirectScheme(): String? {
366+
val applicationInfo = context.packageManager?.getApplicationInfo(context.packageName, PackageManager.GET_META_DATA)
367+
return applicationInfo?.metaData?.get("pace_redirect_scheme")?.toString()
368+
}
369+
354370
private fun stringToAlgorithm(algorithm: String): HmacAlgorithm? {
355371
return when (algorithm) {
356372
"SHA1" -> HmacAlgorithm.SHA1

library/src/main/java/cloud/pace/sdk/utils/Configuration.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ data class Configuration @JvmOverloads constructor(
55
var clientAppVersion: String,
66
var clientAppBuild: String,
77
var apiKey: String,
8-
var clientId: String? = null,
98
var authenticationMode: AuthenticationMode = AuthenticationMode.WEB,
109
var environment: Environment = Environment.PRODUCTION,
1110
var extensions: List<String> = emptyList(),

library/src/test/java/cloud/pace/sdk/appkit/AppWebViewModelTest.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ class AppWebViewModelTest {
4040
private val sharedPreferencesModel = mock(SharedPreferencesModel::class.java)
4141
private val payManager = mock(PayAuthenticationManager::class.java)
4242
private var disabled = ""
43-
private val clientId = "c4b48d7a-5b36-11eb-ae93-0242ac130002"
4443
private val host = "app.test.net"
4544
private val url = "https://$host"
4645
private val eventManager = object : TestAppEventManager() {
@@ -54,7 +53,7 @@ class AppWebViewModelTest {
5453

5554
@Before
5655
fun init() {
57-
PACECloudSDK.configuration = Configuration("", "", "", "", clientId = clientId, environment = Environment.DEVELOPMENT)
56+
PACECloudSDK.configuration = Configuration("", "", "", "", environment = Environment.DEVELOPMENT)
5857

5958
appModel.callback = appCallback
6059
disabled = ""
@@ -297,6 +296,5 @@ class AppWebViewModelTest {
297296
viewModel.handleOpenURLInNewTab(openURLInNewTabRequest)
298297

299298
assertEquals(cancelUrl, viewModel.url.value?.getContentIfNotHandled())
300-
verify(appCallback, times(1)).onCustomSchemeError(context, "pace.$clientId://redirect")
301299
}
302300
}

0 commit comments

Comments
 (0)