I am trying to implement a flow in which i redirect the user to a web page while in the app using
Browser.Default.OpenAsync
And in my flow, I want for example to wait until the user closes that browser page, and continue the rest of my operations. How can I achieve this as I haven't found a method in the official documentation ?
A quick example to understand what the issue is:
private async Task<string> HandleNoFunds()
{
if (!HasAcceptedStripeTerms)
{
//Handle displaying Stripe TOS
}
//Wait until this finishes so the user pays using Stripe, but at the moment it waits until the browser opens
await Browser.Default.OpenAsync(myStripeCustomPageUrl, BrowserLaunchMode.SystemPreferred);
return "success";
}
I found a solution for my use case and I'll post what I did here in case anyone in the future needs it ( note that this only works for IOS, for android, I saw that Web Authenticator could be a workaround, for more read here: https://learn.microsoft.com/en-us/dotnet/maui/platform-integration/communication/authentication?view=net-maui-8.0&tabs=android):
Edit: apart from Web Auth, another possible workaround for Android would be creating a CustomTabsDelegate and binding a BrowserClosedEvent to the CustomTabas events ( if they exist, I haven't done the research on this, it's just a thought)
I created a delegate and an event in my viewModel called BrowserLoaded
along with an event with the same name
public delegate void BrowserLoaded(string url);
public event BrowserLoaded? BrowserLoadedEvent;
Inside my viewModel, in the function that needs to load the browser after validation operations, i invoked the event like this:
private async Task<string> HandleNoFunds()
{
if (!HasAcceptedStripeTerms)
{
//Handle stripe terms
}
var link = $"{ApiPaymentUrl}/?amount={EndUserCost*100}¶m1={param1}...";
BrowserLoadedEvent.Invoke(link);
return AppStatics.Success;
}
Note that this code block is a simplification of what I did, and ApiPaymentUrl is a redirect to a custom react app which integrates Stripe API. Up to this point the implementation is platform-agnostic, but here comes the IOS specific part.
In Platforms/iOS I created a SafariViewControllerDelegate which itself defines a BrowserClosed
event which is invoked when the browser finishes and closes.
public class SafariViewControllerDelegate : SFSafariViewControllerDelegate
{
public event Action BrowserClosed;
public override void DidFinish(SFSafariViewController controller)
{
BrowserClosed?.Invoke();
}
}
Now, switching to my view code behind, i subscribed to my event declared in the viewmodel
public BuyPage(BuyPageViewModel viewModel) : base(viewModel)
{
InitializeComponent();
viewModel.BrowserLoadedEvent +=ViewModel_BrowserLoadedEvent;
}
I created the ViewModel_BrowserLoadedEvent which:
NSUrl
from my string urlSfSafariViewController
and the delegate I createdBrowserClosedEvent
SfSafariViewController
to the screen depending on iOS version private void ViewModel_BrowserLoadedEvent(string url)
{
//Here you can add an implementation for Android if you want to expand this by maybe implementing a CustomTabsDelegate - just an idea
#if IOS
// Ensure that you have a valid URL
if (Uri.TryCreate(url, UriKind.Absolute, out var uri))
{
var nsUrl = new NSUrl(uri.AbsoluteUri);
var safariViewController = new SFSafariViewController(nsUrl);
// Create and set the delegate
var safariDelegate = new SafariViewControllerDelegate();
safariDelegate.BrowserClosed += OnBrowserClosed;
safariViewController.Delegate = safariDelegate;
// Get the current UIWindowScene
UIWindow window = null;
if (UIDevice.CurrentDevice.CheckSystemVersion(13, 0))
{
foreach (var scene in UIApplication.SharedApplication.ConnectedScenes)
{
if (scene is UIWindowScene windowScene)
{
window = windowScene.Windows.FirstOrDefault(w => w.IsKeyWindow);
if (window != null)
{
break;
}
}
}
}
else
{
window = UIApplication.SharedApplication.KeyWindow;
}
if (window != null)
{
var rootViewController = window.RootViewController;
// Find the presented view controller
while (rootViewController.PresentedViewController != null)
{
rootViewController = rootViewController.PresentedViewController;
}
// Present the SFSafariViewController
rootViewController.PresentViewController(safariViewController, true, null);
}
}
#endif
}
Then finally, I defined my OnBrowserClosed
function in my view which I bound on this section:
var safariDelegate = new SafariViewControllerDelegate();
safariDelegate.BrowserClosed += OnBrowserClosed;
safariViewController.Delegate = safariDelegate;
Here is my function:
private async void OnBrowserClosed()
{
//If i don't add delay here the popup won't display
await Task.Delay(1000);
var popup = new CustomPopup();
//Display popup and try to see if balance changed
Shell.Current.ShowPopup(popup);
var verificationResult = await Task.Run(((BuyPageViewModel)ViewModel).VerifyPayment);
if (verificationResult != AppStatics.Success)
{
popup.Close();
if (verificationResult != (string)App.AllResources["Strings"]["PaymentDelays"])
{
await Shell.Current.ShowPopupAsync(new ErrorPopup(verificationResult));
return;
}
await Shell.Current.ShowPopupAsync(new StaticSuccessPopup() { PopupType = StaticPopupType.PaymentDelays});
return;
}
popup.Close();
await ((BuyPageViewModel)ViewModel).HandlePayment();
}
VerifyPayment
in my viewModel does a request which checks if the transaction is showing in our DB as completed so we know if funds have been transfered or if there is an error
HandlePayment
continues the flow of our payment system
I hope this is helpful for anyone that wants to implement such a feature in their MAUI app!