Deep link implementation
Deep linking is a technique that allows a mobile device to respond to a URI and launch a mobile application that corresponds to that URI.
Android handles app-linking through the intent system – when the user clicks on a link in a mobile browser, the mobile browser will dispatch an intent that Android will delegate to a registered application.
Configuring the Intent Filter
You need to set IntentFilterAttribute for your main launcher.
[Activity(Label = "Halk DEM", Icon = "@drawable/icon", Theme = "@style/Theme.Splash", MainLauncher = true, NoHistory = true, ScreenOrientation = Android.Content.PM.ScreenOrientation.Portrait)]
[IntentFilter(new[] { "android.intent.action.VIEW" }, Categories = new[] { "android.intent.action.VIEW", "android.intent.category.BROWSABLE", "android.intent.category.DEFAULT" }, DataSchemes = new[] { "https", "http" }, DataHost = "halkbankatest.24x7.rs", DataPathPrefix = "/ips/ek/fl/", AutoVerify = true)]
public class SplashActivity : Activity, Animator.IAnimatorListener
{
For more information about intent filter configuration and its content, visit this link.
Our example shows intent filter configured to handle https://halkbankatest.24x7.rs/ips/ek/fl/.
Processing data
We implemented deep link for IPS. When user clicks on a subscribed link from mobile browser, application should launch, process the data from the link parameters and navigate to instant payment order's second step, as if the user had scanned the QR code. According to the instructions of NBS, 2 parameters are added to link - data and callback.
Parameter data is base 64 encoded string with data from IPS QR code, while callback contains callback url to which mobile application navigates after the execution of the transaction.
When user clicks on a deep link, the activity that has intent subscribed for that link is started, so in Show() method, which is called by OnCreate() we added OnNewIntent(Intent);.
protected void Show()
{
... your code ...
OnNewIntent(Intent);
}
protected override void OnNewIntent(Intent intent)
{
base.OnNewIntent(intent);
if (intent.DataString != null)
{
GetDeepLinkData(intent.DataString);
}
}
If intent is started by deep link, DataString property will have value of a link, and in GetDeepLinkData() we process the data.
protected void GetDeepLinkData(string dataString)
{
if (!dataString.ToLower().Contains(Global.URI_APP_AUTHORITY.ToLower()) && !dataString.ToLower().Contains(Global.URI_APP_DataPath))
{
return;
}
if (!dataString.Split('?')[0].EndsWith(Global.URI_APP_DataPath) || !dataString.Split('?')[1].StartsWith("data=") || !dataString.ToLower().Contains("callback=") || !dataString.Split("callback")[0].EndsWith("&"))
{
Global.IsWrongDeepLink = true;
}
else
{
Global.HasDeepLinkData = true;
Uri uri = new Uri(dataString);
var query = LabelHtml.Forms.Plugin.Abstractions.HttpUtility.ParseQueryString(uri);
Global.DeepLinkParams = query.Get("data").FirstOrDefault();
if (dataString.Split('&')[0].EndsWith("==") && !Global.DeepLinkParams.EndsWith("=="))
{
Global.DeepLinkParams += "==";
}
else if (dataString.Split('&')[0].EndsWith("=") && !Global.DeepLinkParams.EndsWith("="))
{
Global.DeepLinkParams += "=";
}
Global.DeepLinkCallback = query.Get("callback").FirstOrDefault();
}
}
The data are taken from the link and stored to global variables, which will be further used.
In OnStart() method of your Application.cs, you can set global variables to your values.
Global.URI_APP_SCHEME = "https";
Global.URI_APP_AUTHORITY = "halkbankatest.24x7.rs";
Global.URI_APP_DataPath = "/ips/ek/fl/";
When application is launched, from first view model (StartViewModel) we navigate to IPS or show an error message if deep link is invalid. In StartAsync() method:
if (Global.HasDeepLinkData)
{
if (Global.IsDeepLinkPaymentInitiated && Xamarin.Forms.Device.RuntimePlatform == Xamarin.Forms.Device.iOS)
{
Global.IsDeepLinkPaymentInitiated = false;
Global.HasDeepLinkData = false;
return true;
}
Xamarin.Forms.Device.BeginInvokeOnMainThread(async () =>
{
await App.Device.SetLoadingAsync(() => App.Navigation.NavigateToDeepLinkPage(), App.Translation["core_loading"]);
});
return true;
}
if (Global.IsWrongDeepLink)
{
Global.IsWrongDeepLink = false;
await App.Device?.DisplayMessageAsync(App.Translation?["payment_loaded_data_invalid_for_ips"], App.Translation?["core_error"]);
}
We added Global.IsDeepLinkPaymentInitiated to handle a bug that was reported and case when user clicks on a deep link and then, after application loads, does not confirm payment, but returns to the web page (back button) and clicks the deep link again.
public virtual async Task<bool> NavigateToDeepLinkPage()
{
var result = await NavigateToAsync(NavigationUriParser.Parse(CurrentApplication.Config.FactoryConfig_ViewModelToNavigateFromDeepLink));
return result.Success;
}
You can override FactoryConfig_ViewModelToNavigateFromDeepLink and set your view model name.
After navigating to desired view model, we then provide expected behavior using global variables, decoding base64 QR code string value and so on... You can check changes made in InstantPaymentScanViewModel.
This implementation worked as we expected when the application is closed or is in background and user is not logged in. For case when user is logged in and application is in background, in OnCreate() of main launcher that has subscribed intent filter, we added:
protected override void OnCreate(Bundle savedInstanceState)
{
if (!String.IsNullOrWhiteSpace(Intent.DataString) && App != null && App.Session?.UserIsLoggedIn == true)
{
base.OnCreate(savedInstanceState);
GetDeepLinkData(Intent.DataString);
if (Global.HasDeepLinkData)
{
Xamarin.Forms.Device.BeginInvokeOnMainThread(async () =>
{
await App.Device?.SetLoadingAsync(() => App.Navigation?.NavigateToDeepLinkPage(), App.Translation["core_loading"]);
});
}
if (Global.IsWrongDeepLink)
{
Global.IsWrongDeepLink = false;
Xamarin.Forms.Device.BeginInvokeOnMainThread(async () =>
{
await Task.Delay(1000);
await App.Device?.DisplayMessageAsync(App.Translation?["payment_loaded_data_invalid_for_ips"], App.Translation?["core_error"]);
});
}
SetResult(Result.Canceled);
Finish();
return;
}
... the rest of code you already have in this method ...
}
Because you're already logged in, you don't want to start splash screen and application again, just process the data and then finish this activity and continue with already started one.
Additionally we added a minor change in InstantPaymentWrapper for Android in TransactionFromString method:
public TransactionWrapper TransactionFromString(string transactionString)
{
try
{
return ConvertTo(facade.TransactionFromString(transactionString));
}
catch (Exception ex)
{
Xamarin.Forms.Device.BeginInvokeOnMainThread(async () =>
{
await DEMApplication.Current.Device.DisplayMessageAsync(
DEMApplication.Current.Translation["payment_loaded_data_invalid_for_ips"], DEMApplication.Current.Translation["core_error"]);
await DEMApplication.Current.Navigation.NavigateToParentAsync();
});
return null;
}
}
Universal links on iOS
Here are the high-level steps to get Universal Links working for your app:
Configuring your application to register approved domains
First thing you need to do is to go to apple developer and enable Associated domains on your app identifier.
After you enable this, you'll need to regenerate provisioning profiles that include this identifier, because they will become invalid after this change.
After this change, we needed to generate a new certificates also. Try first with the existing certificates, but if there is any problem with them, just create new ones.
Next things is to add proper domain entitlement. In Entitlements.plist add:
Entilements file must be included in build.
Creating and hosting AASA file
Then, you need to create apple-app-site-association file and host it to your website. This is json file that contains data about your application and URLs that your application can handle. It's a connection with your website and it must be uploaded at the root of web server or in the .well-known subdirectory.
For more about AASA file and other mentioned steps visit this links:
- What is an AASA file?
- How to set up Universal Links to Deep Link on iOS
- Supporting associated domains
- Support Universal Links
Handle Universal Links
ContinueUserActivity(UIApplication application, NSUserActivity userActivity, UIApplicationRestorationHandler completionHandler) in AppDelegate is the method that will receive a link and there you should handle it.
When iOS launches your app after a user taps a universal link, you receive an NSUserActivity object. The activity object’s webpageURL property contains the URL that the user is accessing.
public override bool ContinueUserActivity(UIApplication application, NSUserActivity userActivity, UIApplicationRestorationHandler completionHandler)
{
//base.ContinueUserActivity(application, userActivity, completionHandler);
if (Global.IsDeepLinkPaymentInitiated && App.Session?.UserIsLoggedIn == false)
{
Global.IsDeepLinkPaymentInitiated = false;
}
if (userActivity.WebPageUrl != null)
{
if (App != null)
{
if (userActivity.WebPageUrl.AbsoluteString.Contains("?data=") && userActivity.WebPageUrl.AbsoluteString.Contains("callback="))
{
Global.HasDeepLinkData = true;
if (!userActivity.WebPageUrl.AbsoluteString.Split('?')[0].EndsWith(Global.URI_APP_DataPath) || !userActivity.WebPageUrl.AbsoluteString.Split('?')[1].StartsWith("data=") || !userActivity.WebPageUrl.AbsoluteString.Split("callback")[0].EndsWith("&"))
{
Global.HasDeepLinkData = false;
Global.IsWrongDeepLink = true;
}
if (Global.HasDeepLinkData)
{
Uri uri = new Uri(userActivity.WebPageUrl.AbsoluteString);
var query = LabelHtml.Forms.Plugin.Abstractions.HttpUtility.ParseQueryString(uri);
Global.DeepLinkParams = query.Get("data").FirstOrDefault();
if (userActivity.WebPageUrl.AbsoluteString.Split('&')[0].EndsWith("==") && !Global.DeepLinkParams.EndsWith("=="))
{
Global.DeepLinkParams += "==";
}
else if (userActivity.WebPageUrl.AbsoluteString.Split('&')[0].EndsWith("=") && !Global.DeepLinkParams.EndsWith("="))
{
Global.DeepLinkParams += "=";
}
Global.DeepLinkCallback = query.Get("callback").FirstOrDefault();
}
if (App.Session != null && App.Session?.UserIsLoggedIn == true)
{
if (Global.IsWrongDeepLink)
{
Global.IsWrongDeepLink = false;
Xamarin.Forms.Device.BeginInvokeOnMainThread(async () =>
{
await Task.Delay(1000);
await App.Device.DisplayMessageAsync(App.Translation["payment_loaded_data_invalid_for_ips"], App.Translation["core_error"]);
});
}
else
{
if (Global.IsDeepLinkPaymentInitiated)
{
//Global.IsDeepLinkPaymentInitiated = false;
Global.HasDeepLinkData = false;
return true;
}
Xamarin.Forms.Device.BeginInvokeOnMainThread(async () =>
{
await App.Device.SetLoadingAsync(() => App.Navigation.NavigateToDeepLinkPage(), App.Translation["core_loading"]);
});
}
}
return true;
}
else
{
Xamarin.Forms.Device.BeginInvokeOnMainThread(async () =>
{
await Task.Delay(1000);
await App.Device.DisplayMessageAsync(App.Translation["payment_loaded_data_invalid_for_ips"], App.Translation["core_error"]);
});
}
}
}
return false;
}
This worked as expected on our product which doesn't have obscure display enabled (preventing from taking screenshots inside the app and showing a blank screen when app is in background), but on Halk we had some problems, so in this case, beside ContinueUserActivity we have:
private bool _fromBackground = false;
private UIView _view;
override public void OnResignActivation(UIApplication uiApplication)
{
var window = UIApplication.SharedApplication.KeyWindow;
var rect = UIScreen.MainScreen.Bounds;
_view = new UIView { Frame = rect };
_view.BackgroundColor = UIColor.FromRGB(60, 179, 113);
window.AddSubview(_view);
}
override public void OnActivated(UIApplication uiApplication)
{
if (_view != null)
{
_view.RemoveFromSuperview();
_view.Dispose();
}
if (Global.HasDeepLinkData && App?.Session != null && App?.Session?.UserIsLoggedIn == false)
{
Xamarin.Forms.Device.BeginInvokeOnMainThread(async () =>
{
if (_fromBackground == true)
{
await App.Device.SetLoadingAsync(() => App.Navigation.NavigateToDeepLinkPage(), App.Translation["core_loading"]);
}
else
{
await Task.Delay(1000);
}
});
}
if (Global.IsWrongDeepLink && App?.Session != null && App?.Session?.UserIsLoggedIn == false)
{
Xamarin.Forms.Device.BeginInvokeOnMainThread(async () =>
{
if (_fromBackground == true)
{
Global.IsWrongDeepLink = false;
await App.Device.DisplayMessageAsync(App.Translation["payment_loaded_data_invalid_for_ips"], App.Translation["core_error"]);
}
else
{
await Task.Delay(1000);
}
});
}
}
public override void WillEnterForeground(UIApplication uiApplication)
{
_fromBackground = true;
base.WillEnterForeground(uiApplication);
}
Also, we had to make some changes regarding Global.IsDeepLinkPaymentInitiated in ContinueUserActivity and PaymentConfirmationPage.xaml.cs:
public override bool ContinueUserActivity(UIApplication application, NSUserActivity userActivity, UIApplicationRestorationHandler completionHandler)
{
//base.ContinueUserActivity(application, userActivity, completionHandler);
if (userActivity.WebPageUrl != null)
{
if (App != null)
{
if (userActivity.WebPageUrl.AbsoluteString.Contains("?data=") && userActivity.WebPageUrl.AbsoluteString.Contains("callback="))
{
Global.HasDeepLinkData = true;
if (!userActivity.WebPageUrl.AbsoluteString.Split('?')[0].EndsWith(Global.URI_APP_DataPath) || !userActivity.WebPageUrl.AbsoluteString.Split('?')[1].StartsWith("data=") || !userActivity.WebPageUrl.AbsoluteString.Split("callback")[0].EndsWith("&"))
{
Global.HasDeepLinkData = false;
Global.IsWrongDeepLink = true;
}
if (Global.IsDeepLinkPaymentInitiated && App.Session?.UserIsLoggedIn == false)
{
//Global.IsDeepLinkPaymentInitiated = false;
Global.HasDeepLinkData = false;
}
if (Global.HasDeepLinkData)
{
Uri uri = new Uri(userActivity.WebPageUrl.AbsoluteString);
var query = LabelHtml.Forms.Plugin.Abstractions.HttpUtility.ParseQueryString(uri);
Global.DeepLinkParams = query.Get("data").FirstOrDefault();
if (userActivity.WebPageUrl.AbsoluteString.Split('&')[0].EndsWith("==") && !Global.DeepLinkParams.EndsWith("=="))
{
Global.DeepLinkParams += "==";
}
else if (userActivity.WebPageUrl.AbsoluteString.Split('&')[0].EndsWith("=") && !Global.DeepLinkParams.EndsWith("="))
{
Global.DeepLinkParams += "=";
}
Global.DeepLinkCallback = query.Get("callback").FirstOrDefault();
}
if (App.Session != null && App.Session?.UserIsLoggedIn == true)
{
if (Global.IsWrongDeepLink)
{
Global.IsWrongDeepLink = false;
Xamarin.Forms.Device.BeginInvokeOnMainThread(async () =>
{
await App.Device.DisplayMessageAsync(App.Translation["payment_loaded_data_invalid_for_ips"], App.Translation["core_error"]);
});
}
else
{
if (Global.IsDeepLinkPaymentInitiated)
{
//Global.IsDeepLinkPaymentInitiated = false;
Global.HasDeepLinkData = false;
return true;
}
Xamarin.Forms.Device.BeginInvokeOnMainThread(async () =>
{
await App.Device.SetLoadingAsync(() => App.Navigation.NavigateToDeepLinkPage(), App.Translation["core_loading"]);
});
}
}
return true;
}
else
{
Xamarin.Forms.Device.BeginInvokeOnMainThread(async () =>
{
await App.Device.DisplayMessageAsync(App.Translation["payment_loaded_data_invalid_for_ips"], App.Translation["core_error"]);
});
}
}
}
return false;
}
protected override void OnDisappearing()
{
base.OnDisappearing();
if (BindingContext is InstantPaymentScanViewModel vM)
{
if (Xamarin.Forms.Device.RuntimePlatform == Xamarin.Forms.Device.iOS || !vM.IsPreLogin)
{
Core.Global.IsDeepLinkPaymentInitiated = false;
}
}
}
When obscure display is enabled on iOS we need to set Global.IsDeepLinkPaymentInitiated to false in OnDisappearing() whether it is a prelogin or a postlogin, so in Halk's Application.cs we added:
typeCatalog.AddOrUpdate(
new ViewModelInfo(typeof(InstantPaymentScanViewModel))
{
PageTypes = new Type[]{
typeof(InstantPaymentScanPage),
typeof(PaymentConfirmationPageOverride),
typeof(PostTransactionPage)
},
ShowLoading = true
});
We are trying to find a better solution to this.
We hope that this helps you, happy coding!