Skip to content

Commit

Permalink
✨ Mobile | Add deep linking (#1050)
Browse files Browse the repository at this point in the history
* Initial code

* Add todo

* Handle custom URL

* Prepend QR codes with the custom URL

* Cleanup
  • Loading branch information
zacharykeeping authored Sep 19, 2024
1 parent 4be2786 commit ddff5de
Show file tree
Hide file tree
Showing 10 changed files with 132 additions and 5 deletions.
5 changes: 4 additions & 1 deletion src/AdminUI/Components/AdminQRCode.razor
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@
return;
}

// Prepend the code with the custom redeem URL
var url = "sswrewards://redeem?code=" + QRCodeString;

QRCodeGenerator qrGenerator = new QRCodeGenerator();
QRCodeData qrCodeData = qrGenerator.CreateQrCode(QRCodeString, QRCodeGenerator.ECCLevel.Q);
QRCodeData qrCodeData = qrGenerator.CreateQrCode(url, QRCodeGenerator.ECCLevel.Q);
PngByteQRCode qrCode = new PngByteQRCode(qrCodeData);
byte[] qrCodeAsPngByteArr = qrCode.GetGraphic(20);
QRCodeStr = $"data:image/png;base64,{Convert.ToBase64String(qrCodeAsPngByteArr)}";
Expand Down
5 changes: 4 additions & 1 deletion src/MobileUI/Common/ImageHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ public static class ImageHelpers
{
public static ImageSource GenerateQrCode(string qrCodeString)
{
// Prepend the code with the custom redeem URL
var url = "sswrewards://redeem?code=" + qrCodeString;

using QRCodeGenerator qrGenerator = new();
using QRCodeData qrCodeData = qrGenerator.CreateQrCode(qrCodeString, QRCodeGenerator.ECCLevel.Q);
using QRCodeData qrCodeData = qrGenerator.CreateQrCode(url, QRCodeGenerator.ECCLevel.Q);
using PngByteQRCode qrCode = new(qrCodeData);

byte[] qrCodeBytes = qrCode.GetGraphic(20);
Expand Down
30 changes: 28 additions & 2 deletions src/MobileUI/Features/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
namespace SSW.Rewards.Mobile;
using Mopups.Services;

namespace SSW.Rewards.Mobile;

public partial class App : Application
{
private static IServiceProvider _provider;
private static IAuthenticationService _authService;
public static object UIParent { get; set; }

public App(LoginPage page)
public App(LoginPage page, IServiceProvider serviceProvider, IAuthenticationService authService)
{
_provider = serviceProvider;
_authService = authService;

InitializeComponent();
Current.UserAppTheme = AppTheme.Dark;

Expand Down Expand Up @@ -33,6 +40,25 @@ protected override void OnResume()
{
// Handle when your app resumes
}

protected override async void OnAppLinkRequestReceived(Uri uri)
{
base.OnAppLinkRequestReceived(uri);

var queryDictionary = System.Web.HttpUtility.ParseQueryString(uri.Query);
var code = queryDictionary.Get("code");

if (_authService.IsLoggedIn)
{
var vm = ActivatorUtilities.CreateInstance<ScanResultViewModel>(_provider);
var popup = new PopupPages.ScanResult(vm, code);
await MopupService.Instance.PushAsync(popup);
}
else
{
((LoginPage)MainPage)?.QueueCodeScan(code);
}
}

private async Task CheckApiCompatibilityAsync()
{
Expand Down
5 changes: 5 additions & 0 deletions src/MobileUI/Features/Login/LoginPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,9 @@ await Task.WhenAny<bool>
await _viewModel.Refresh();
}
}

public void QueueCodeScan(string code)
{
_viewModel.QueueCodeScan(code);
}
}
22 changes: 21 additions & 1 deletion src/MobileUI/Features/Login/LoginPageViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.AppCenter.Crashes;
using Mopups.Services;

namespace SSW.Rewards.Mobile.ViewModels;

Expand All @@ -9,6 +10,9 @@ public partial class LoginPageViewModel : BaseViewModel
private readonly IAuthenticationService _authService;
private readonly IPushNotificationsService _pushNotificationsService;
private readonly IPermissionsService _permissionsService;
private readonly IServiceProvider _provider;

private string _pendingScanCode;

[ObservableProperty]
private bool _isRunning;
Expand All @@ -25,14 +29,21 @@ public LoginPageViewModel(
IAuthenticationService authService,
IUserService userService,
IPushNotificationsService pushNotificationsService,
IPermissionsService permissionsService)
IPermissionsService permissionsService,
IServiceProvider provider)
{
_authService = authService;
_pushNotificationsService = pushNotificationsService;
_permissionsService = permissionsService;
_provider = provider;
ButtonText = "Sign up / Log in";
userService.MyQrCodeObservable().Subscribe(myQrCode => _isStaff = !string.IsNullOrWhiteSpace(myQrCode));
}

public void QueueCodeScan(string code)
{
_pendingScanCode = code;
}

[RelayCommand]
private async Task LoginTapped()
Expand Down Expand Up @@ -126,6 +137,15 @@ private async Task OnAfterLogin()
{
await UploadDeviceTokenIfRequired();
}

// Handle qr code received before login
if (!string.IsNullOrEmpty(_pendingScanCode))
{
var vm = ActivatorUtilities.CreateInstance<ScanResultViewModel>(_provider);
var popup = new PopupPages.ScanResult(vm, _pendingScanCode);
_pendingScanCode = null;
await MopupService.Instance.PushAsync(popup);
}
}

/// <summary>
Expand Down
41 changes: 41 additions & 0 deletions src/MobileUI/MauiProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public static MauiApp CreateMauiApp()
.UsePageResolver()
.UseBarcodeScanning()
.RegisterFirebase()
.RegisterUrlHandling()
.ConfigureMauiHandlers((handlers) =>
{
handlers.AddHandler(typeof(TableView), typeof(CustomTableViewRenderer));
Expand Down Expand Up @@ -109,4 +110,44 @@ private static MauiAppBuilder RegisterFirebase(this MauiAppBuilder builder)

return builder;
}

private static MauiAppBuilder RegisterUrlHandling(this MauiAppBuilder builder)
{
builder.ConfigureLifecycleEvents(events =>
{
#if IOS
events.AddiOS(ios =>
{
ios.OpenUrl((app, url, options) => HandleAppLink(url.AbsoluteString));
});
#else
events.AddAndroid(android => android.OnCreate((activity, bundle) => {
var action = activity.Intent?.Action;
var data = activity.Intent?.Data?.ToString();

if (action != Android.Content.Intent.ActionView || data is null)
{
return;
}

activity.Finish();
Task.Run(() => HandleAppLink(data));
}));
#endif
});

return builder;
}

static bool HandleAppLink(string url)
{
if (!Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out var uri))
{
return false;
}

App.Current?.SendOnAppLinkRequestReceived(uri);
return true;

}
}
8 changes: 8 additions & 0 deletions src/MobileUI/Platforms/Android/MainActivity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@

namespace SSW.Rewards.Mobile;

[IntentFilter([Android.Content.Intent.ActionView],
Categories =
[
Android.Content.Intent.ActionView,
Android.Content.Intent.CategoryDefault,
Android.Content.Intent.CategoryBrowsable
],
DataScheme = "sswrewards", DataHost = "", DataPathPrefix = "/")]
[Activity(Theme = "@style/MyTheme.Splash", MainLauncher = true,
ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode |
ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density,
Expand Down
11 changes: 11 additions & 0 deletions src/MobileUI/Platforms/iOS/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,16 @@
<string>SSW Rewards</string>
<key>CFBundleDisplayName</key>
<string>SSW Rewards</string>
<key>CFBundleIdentifier</key>
<string></string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>sswrewards</string>
</array>
</dict>
</array>
</dict>
</plist>
3 changes: 3 additions & 0 deletions src/MobileUI/Services/AuthenticationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public interface IAuthenticationService
Task<string> GetAccessToken();
Task SignOut();
bool HasCachedAccount { get; }
bool IsLoggedIn { get; }
event EventHandler DetailsUpdated;
}

Expand All @@ -26,6 +27,8 @@ public class AuthenticationService : IAuthenticationService

public event EventHandler DetailsUpdated;
public bool HasCachedAccount { get => Preferences.Get(nameof(HasCachedAccount), false); }

public bool IsLoggedIn { get => !string.IsNullOrWhiteSpace(_accessToken); }

public AuthenticationService(IBrowser browser)
{
Expand Down
7 changes: 7 additions & 0 deletions src/MobileUI/Services/ScannerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,13 @@ private async Task<ScanResponseViewModel> PostRewardAsync(string rewardString, b

public async Task<ScanResponseViewModel> ValidateQRCodeAsync(string qrCodeData)
{
if (qrCodeData.StartsWith("sswrewards://"))
{
var uri = new Uri(qrCodeData);
var queryDictionary = System.Web.HttpUtility.ParseQueryString(uri.Query);
qrCodeData = queryDictionary.Get("code");
}

var decodedQR = StringHelpers.Base64Decode(qrCodeData);

if (decodedQR.StartsWith("ach:"))
Expand Down

0 comments on commit ddff5de

Please sign in to comment.