Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug] MAUI App Crashes/Freezes on Debugging (iOS & Android) with Real Device #27204

Closed
ikeoriaku123 opened this issue Jan 17, 2025 · 10 comments
Closed
Labels
platform/android 🤖 platform/iOS 🍎 platform/macOS 🍏 macOS / Mac Catalyst potential-regression This issue described a possible regression on a currently supported version., verification pending s/needs-attention Issue has more information and needs another look s/triaged Issue has been reviewed t/bug Something isn't working

Comments

@ikeoriaku123
Copy link

ikeoriaku123 commented Jan 17, 2025

Description

When running a .NET MAUI app on a real iOS or Android device using Visual Studio Code in debug mode, the app runs successfully only after following a strict sequence:

Delete bin and obj folders
Perform a clean build
Restore dependencies
Rebuild the project
Delete the app from the device
Deploy and run the app

However, when attempting to re-run the app without repeating the above steps, the application either crashes on startup or freezes. This forces me to repeat the entire cycle again.

Steps to Reproduce

  1. Run the MAUI app on a real device (iOS/Android) using Visual Studio Code in debug mode.
  2. The app works only after deleting bin/obj, cleaning, restoring, rebuilding, and deleting the app from the device.
  3. Stop debugging and attempt to run the app again.
  4. The app either crashes on launch or freezes.
  5. Repeating the cleanup cycle is necessary for it to work again.

Expected Behavior
The app should run consistently in debug mode without requiring a full clean-up and rebuild every time.
Actual Behavior
The app crashes or freezes unless the cleanup steps are performed before every debug session.

Additional Information
.NET MAUI Version: [Provide your MAUI version]
Visual Studio Code Version: [Provide VS Code version]
Operating System: [Windows/macOS + version]
Target Devices: [iOS version / Android version & model]
Any Exception Logs?: [Attach logs if available]

Link to public reproduction project repository

No response

Version with bug

9.0.14 SR1.4

Is this a regression from previous behavior?

Yes, this used to work in .NET MAUI

Last version that worked well

8.0.90 SR9

Affected platforms

iOS, Android, macOS

Affected platform versions

iOS 15.8, iOS 18, Android 10

Did you find any workaround?

The workaround is going through that annoying cycle

Relevant log output


You may only use the Microsoft Visual Studio .NET/C/C++ Debugger (vsdbg) with
Visual Studio Code, Visual Studio or Visual Studio for Mac software to help you
develop and test your applications.

The program 'App.Client.Maui.dll' has exited with code 0 (0x0).
[rom.arcrom] Late-enabling -Xcheck:jni
[rom.arcrom] Unquickening 22 vdex files!
[rom.arcrom] The ClassLoaderContext is a special shared library.
[LoadedApk] LoadedApk::makeApplication() appContext=android.app.ContextImpl@c5c149e appContext.mOpPackageName=com.mypackage appContext.mBasePackageName=com.mypackage appContext.mPackageInfo=android.app.LoadedApk@1b11b7f
2
[NetworkSecurityConfig] No Network Security Config specified, using platform default
[DOTNET] AndroidCryptoNative_InitLibraryOnLoad: jint AndroidCryptoNative_InitLibraryOnLoad(JavaVM *, void *) in /__w/1/s/src/native/libs/System.Security.Cryptography.Native.Android/pal_jni.c
[DOTNET] GetOptionalClassGRef: optional class com/android/org/conscrypt/OpenSSLEngineImpl was not found
[monodroid] Failed to create directory '/data/user/0/com.mypackage/files/.override/armeabi-v7a'. File exists
[monodroid] Creating public update directory: /data/user/0/com.mypackage/files/.__override__/armeabi-v7a
[rom.arcrom] Attempt to remove non-JNI local reference, dumping thread
[monodroid-debug] Trying to initialize the debugger with options: --debugger-agent=transport=dt_socket,loglevel=0,address=127.0.0.1:51507,server=y,embedding=1,timeout=-459748843

@ikeoriaku123 ikeoriaku123 added the t/bug Something isn't working label Jan 17, 2025
@ikeoriaku123
Copy link
Author

This is also happening in version 9.030

@jfversluis
Copy link
Member

Without any reproduction or logs of any kind it will be impossible for us to determine what might be going on here or where the source of this issue might be.

If you revert back to 8.0.90 SR9 does it then work again? Or is it still broken now in this state? Are you able to test this project on Windows with Visual Studio? Does that show the same behavior? Does it only happen with a certain project or if you start a new project does it happen there as well?

@jfversluis jfversluis added the s/needs-info Issue needs more info from the author label Jan 17, 2025
@ikeoriaku123
Copy link
Author

@jfversluis I haven't reverted back to 8.0.90 SR9. I still do get the issue with 8.0.90 SR9 but not as frequent as now. The interesting is that I am not seeing any logs or any reason for the crash which makes it more difficult. I have tried deleting and reinstalling visual studio code but to no avail. I cannot use visual studio for mac as it's been retired. Am I the only one experiencing this issue. I know I have seen some complaining but not on github

@ikeoriaku123 ikeoriaku123 reopened this Jan 17, 2025
@samhouts samhouts added platform/macOS 🍏 macOS / Mac Catalyst platform/android 🤖 platform/iOS 🍎 potential-regression This issue described a possible regression on a currently supported version., verification pending labels Jan 17, 2025
@ikeoriaku123
Copy link
Author

@jfversluis please find logs updated at Relevant log output. The crash is more frequent with maui android and UIs becoming unresponsive. I need help. I don't want to give up on maui. I will continue to update logs if different

@dotnet-policy-service dotnet-policy-service bot added s/needs-attention Issue has more information and needs another look and removed s/needs-info Issue needs more info from the author labels Jan 19, 2025
@Zhanglirong-Winnie
Copy link

Could you provide us with a sample project so we can investigate it further? Looking forward to your reply!

@Zhanglirong-Winnie Zhanglirong-Winnie added the s/triaged Issue has been reviewed label Jan 20, 2025
@jfversluis
Copy link
Member

Unfortunately it would seem like that so far you're the only one or one of a few people that seem to experience this. At least, I haven't heard it before. Do you have the option to try on another machine? Does it behave better there?

If you run a Windows/macOS app? Does it freeze then? And what exactly freezes? The .NET MAUI app you have deployed or Visual Studio Code as a whole? Does it happen with all .NET MAUI projects or just 1 in particular?

@jfversluis jfversluis added s/needs-info Issue needs more info from the author and removed s/needs-attention Issue has more information and needs another look labels Jan 20, 2025
@dotnet-policy-service dotnet-policy-service bot added the s/no-recent-activity Issue has had no recent activity label Jan 27, 2025
@ikeoriaku123
Copy link
Author

@jfversluis sorry for my late response. I think I have found a temporary workaround. I was able to resolve it

Issue 1: [NetworkSecurityConfig] No Network Security Config specified, using platform default

Cause
This warning occurs because no custom Network Security Configuration is provided in the app. By default, Android relies on the platform's default security configuration.

Resolution
Defined a Network Security Configuration: Created a file named network_security_config.xml in the res/xml directory of my Android project:

xml
Copy

example.com Referenced the Configuration in the Manifest: Add the following line to the tag in the AndroidManifest.xml:

xml
<application
android:networkSecurityConfig="@xml/network_security_config"
... >
Optional: Use cleartextTrafficPermitted="false" for better security unless cleartext (HTTP) is required.

Issue 2: Creating public update directory: /data/user/0/com.mypackage/files/.override/armeabi-v7a

Cause
This issue is related to leftover files in the .override directory from a previous build or deployment, causing verbose logs during runtime. This directory is used by .NET MAUI for dynamic updates during development but isn't needed for production.

Resolution
To clean the .override directory and resolve the issue, execute the following commands in ADB (Android Debug Bridge):

Remove the .override Directory:

bash
adb shell run-as com.mypackage rm -rf /data/user/0/com.mypackage/files/.override

Clear the App Data:

bash
adb shell pm clear com.mypackage
This ensures that all cached and temporary files related to the app are removed.

Uninstall the App:

bash
adb uninstall com.mypackage
This removes the app from the device entirely, including residual files from previous installations.

Reinstall the App: Deploy the app again using your development environment (e.g., Visual Studio Code).

I'm now trying on a windows laptop using Visual Studio 2022

@dotnet-policy-service dotnet-policy-service bot added s/needs-attention Issue has more information and needs another look and removed s/needs-info Issue needs more info from the author s/no-recent-activity Issue has had no recent activity labels Jan 28, 2025
@ikeoriaku123
Copy link
Author

ikeoriaku123 commented Jan 30, 2025

Hi @jfversluis I have reverted to my original code, and it works very well. So, it must have been some NuGet packages I installed that caused the issues.
I have been trying to implement Apple Pay and Google Pay for .NET MAUI. I successfully integrated Apple Pay but have been struggling with Google Pay. As far as I know, there isn't an existing implementation of Google Pay for MAUI.
I follow you on LinkedIn and YouTube. By any chance, have you implemented these payment methods? I couldn't find any existing solutions.
I'm considering putting this on hold as it's breaking my code, but feel free to use these implementations as a starting point for Wallet Pay.

APPLE PAY:
using System;
using projectA.Client.Core.Infrastructure.Abstractions.Appstores;
using projectA.Client.Maui.Helpers;
using System.Text.Json;
using Shiny.Reflection;
using static UIKit.UIGestureRecognizer;

#if IOS || MACCATALYST
using Foundation;
using PassKit;
using ObjCRuntime;
using UIKit;
#endif
using projectA.Client.Core.Configurations;

namespace projectA.Client.Maui;

public class ApplePayService : PKPaymentAuthorizationViewControllerDelegate, IPayService
{
private readonly IConfigurationManager configurationManager;
double PayAmount = 1;

readonly NSString[] supportedNetworks ={
        PKPaymentNetwork.Amex,
        PKPaymentNetwork.Discover,
        PKPaymentNetwork.MasterCard,
        PKPaymentNetwork.Visa,
        PKPaymentNetwork.IDCredit,
        PKPaymentNetwork.Interac,
        PKPaymentNetwork.Jcb,
        PKPaymentNetwork.QuicPay
    };

bool isSuccess = false;

public Action<string> AuthorizationComplete { get; set; }
public Action AuthorizationFailed { get; set; }
public bool CanMakePayments => PKPaymentAuthorizationViewController.CanMakePaymentsUsingNetworks(supportedNetworks);

public Action<object, EventArgs> CanMakePaymentsUpdated { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }

public ApplePayService(
    IConfigurationManager configurationManager
)
{
    this.configurationManager = configurationManager;
}

public void AuthorizePayment(double amount, string currencyCode = "")
{
    if (!CanMakePayments)
    {
        Console.WriteLine($"Cannot make payment in AuthorizePayment");
        ShowAuthorizationAlert();
        return;
    }
    try
    {
        PayAmount = amount;
        isSuccess = false;
        // Set up our payment request.
        var paymentRequest = new PKPaymentRequest
        {
            // Our merchant identifier needs to match what we previously set up in
            // the Capabilities window (or the developer portal).
            MerchantIdentifier = this.configurationManager.GlobalSettings.Wallets.ApplePay.MerchantIdentifier,
            // Both country code and currency code are standard ISO formats. Country
            // should be the region you will process the payment in. Currency should
            // be the currency you would like to charge in.
            CountryCode = LocaleHelper.GetRegionInfoByCurrencyCode(currencyCode).CountryCode,
            CurrencyCode = LocaleHelper.GetRegionInfoByCurrencyCode(currencyCode).CurrencyCode,

            // The networks we are able to accept.
            SupportedNetworks = supportedNetworks,

            // Ask your payment processor what settings are right for your app. In
            // most cases you will want to leave this set to ThreeDS.
            MerchantCapabilities = PKMerchantCapability.ThreeDS,
            //     AddToLog("Adding Item To Payment");

            // An array of `PKPaymentSummaryItems` that we'd like to display on the
            // sheet (see the MakeSummaryItems method).
            PaymentSummaryItems = MakeSummaryItems(false),

            // Request shipping information, in this case just postal address.
            //paymentRequest.RequiredShippingAddressFields = PKAddressField.PostalAddress;
        };

        // Display the view controller.
        var viewController = new PKPaymentAuthorizationViewController(paymentRequest);
        viewController.Delegate = this;

        var rootController = WindowStateManager.Default.GetCurrentUIViewController();
        rootController.PresentViewController(viewController, true, null);

    }
    catch (Exception ex)
    {
        Console.WriteLine($"Error in AuthorizePayment: {ex}");
        ShowAuthorizationAlert();
    }
}

PKPaymentSummaryItem[] MakeSummaryItems(bool requiresInternationalSurcharge)
{
    var items = new List<PKPaymentSummaryItem>();

    var productSummaryItem = PKPaymentSummaryItem.Create("Sub-total", new NSDecimalNumber(PayAmount));
    items.Add(productSummaryItem);

    var totalSummaryItem = PKPaymentSummaryItem.Create("projectA", productSummaryItem.Amount);
    items.Add(totalSummaryItem);

    return items.ToArray();
}

[Export("paymentAuthorizationViewController:didAuthorizePayment:handler:")]
public override void DidAuthorizePayment2(PKPaymentAuthorizationViewController controller, PKPayment payment, Action<PKPaymentAuthorizationResult> completion)
{
    var paymentToken = payment.Token;
    var data = payment.Token.PaymentData;
    var paymentResult = new PKPaymentAuthorizationResult(PKPaymentAuthorizationStatus.Success, null);
    completion(paymentResult);
    isSuccess = true;
    AuthorizationComplete?.Invoke(data.ToString());
}

[Export("paymentAuthorizationViewControllerDidFinish:")]
public override void PaymentAuthorizationViewControllerDidFinish(PKPaymentAuthorizationViewController controller)
{
    if (!isSuccess) AuthorizationFailed?.Invoke();
    controller.DismissViewController(true, null);
}

void ShowAuthorizationAlert()
{
    try
    {
        var alert = UIAlertController.Create("Error", "This device cannot make payments.", UIAlertControllerStyle.Alert);
        var action = UIAlertAction.Create("Okay", UIAlertActionStyle.Default, null);
        alert.AddAction(action);

        var rootController = WindowStateManager.Default.GetCurrentUIViewController();
        rootController.PresentViewController(alert, true, null);
        AuthorizationFailed?.Invoke();
    }
    catch (Exception ex) { Console.Write(ex); }
}

public void Initialize(object activity)
{
    throw new NotImplementedException();
}

}

GOOGLE PAY:
using System;
using Android.Content;
using Android.Gms.Wallet;
using AndroidX.AppCompat.App;
using projectA.Client.Core.Infrastructure.Abstractions.Appstores;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using projectA.Client.Core.Configurations;
using Microsoft.Extensions.Logging;
using projectA.Client.Maui.Helpers;
using System.Diagnostics;
using AndroidX.Activity.Result;
using AndroidX.Activity.Result.Contract;
using Android.Gms.Tasks;
using Task = Android.Gms.Tasks.Task;
using Android.App;
using projectA.Client.Core.Extensions;

namespace projectA.Client.Maui;

public class GooglePayService : AppCompatActivity, IPayService, IOnCompleteListener
{
private Context _context;
protected IConfigurationManager ConfigurationManager { get; }
private ActivityResultLauncher paymentLauncher;
private AppCompatActivity _activity;
public Action AuthorizationComplete { get; set; }
public Action AuthorizationFailed { get; set; }
public Action<object, EventArgs> CanMakePaymentsUpdated { get; set; }
public double Amount { get; set; }
protected PaymentsClient PaymentsClient { get; set; }

public GooglePayService(
    IConfigurationManager configurationManager)
{
    _context = Platform.CurrentActivity ?? throw new InvalidOperationException("No valid Activity context.");
    ConfigurationManager = configurationManager;
    InitiateGooglePay();
}

public void Initialize(object activity)
{
    if (_activity != null) return; // Prevent multiple registrations

    _activity = (AppCompatActivity)activity;
    paymentLauncher = _activity.RegisterForActivityResult(
        new ActivityResultContracts.StartActivityForResult(),
        new ActivityResultCallback(HandlePaymentResult));
}

private void InitiateGooglePay()
{
    var walletOptions = new WalletClass.WalletOptions.Builder()

#if DEBUG || TEST
.SetEnvironment(WalletConstants.EnvironmentTest)
#else
.SetEnvironment(WalletConstants.EnvironmentProduction)
#endif
.Build();

    if (PaymentsClient == null)
        PaymentsClient = WalletClass.GetPaymentsClient(_context, walletOptions);

    var readyToPayRequest = IsReadyToPayRequest.FromJson(GetReadyToPayRequest());
    var task = PaymentsClient.IsReadyToPay(readyToPayRequest);

    task.AddOnCompleteListener(this);

    //WeakReferenceMessenger.Default.Register<IntentMessage>(this, (r, m) => OnAuthorizationComplete(r, m));
}

public bool CanMakePayments { get; set; }

public void AuthorizePayment(double amount, string currencyCode = "")
{
    try
    {
        //InitiateGooglePay();
        if (paymentLauncher == null)
            throw new InvalidOperationException("GooglePayService is not initialized. Call Initialize() first.");

        Debug.WriteLine($"Platform.CurrentActivity: {Platform.CurrentActivity}");
        var paymentDataRequest = CreatePaymentDataRequest(amount, currencyCode);
        var task = PaymentsClient.LoadPaymentData(paymentDataRequest);
        //task.AddOnCompleteListener(this);
        task.AddOnCompleteListener(new OnCompleteListener(task =>
        {
            if (task.IsSuccessful && task.Result != null)
            {
                var paymentData = (PaymentData)task.Result;

                // Intent intent = new Intent(Intent.ActionView);
                // intent.PutExtra("EXTRA_PAYMENT_DATA_REQUEST", paymentDataRequest.ToJson());
                // MainThread.BeginInvokeOnMainThread(() =>
                // {
                //     paymentLauncher.Launch(intent);
                // });

            }
            else if (task.IsCanceled)
            {
                Debug.WriteLine("Google Pay transaction cancelled.");
                // var intent = new Intent(Intent.ActionAppError);
                // intent.PutExtra("ERROR_PAYMENT_DATA_REQUEST", paymentDataRequest.ToJson());
                // paymentLauncher.Launch(intent);
            }
            else
            {
                Debug.WriteLine("Google Pay transaction failed.");
                // var intent = new Intent(Intent.ActionAppError);
                // intent.PutExtra("EXTRA_PAYMENT_DATA_REQUEST", paymentDataRequest.ToJson());
                // MainThread.BeginInvokeOnMainThread(() =>
                // {
                //     paymentLauncher.Launch(intent);
                // });
                // Serialize the PaymentDataRequest to JSON
                var jsonRequest = paymentDataRequest.ToJson();

                // Create a new Intent
                //var intent = new Intent("com.google.android.gms.wallet.ACTION_LOAD_PAYMENT_DATA");
                var intent = new Intent(Intent.ActionView);
                //intent.SetPackage("com.google.android.gms");
                intent.SetPackage("com.packagename");
                intent.PutExtra("EXTRA_PAYMENT_DATA_REQUEST", jsonRequest);

                MainThread.BeginInvokeOnMainThread(() =>
                {
                    paymentLauncher.Launch(intent);
                });
            }
        }));
    }
    catch (Exception ex)
    {
        Debug.WriteLine($"Error launching Google Pay: {ex.Message}");
        AuthorizationFailed?.Invoke();
    }
}

public void SetPaymentLauncher(ActivityResultLauncher launcher)
{
    paymentLauncher = launcher ?? throw new ArgumentNullException(nameof(launcher));
}

public void OnComplete(Task completeTask)
{
    if (completeTask.IsComplete && (bool)completeTask.Result)
    {
        CanMakePayments = completeTask.IsSuccessful;
        CanMakePaymentsUpdated?.Invoke(this, null);
    }
    else
    {
        Debug.WriteLine("Google Pay is not available.");
    }
}

public string GetReadyToPayRequest() => JsonConvert.SerializeObject(GetBaseRequest());

/// <summary>
/// 1. Define your Google Pay API Version
/// </summary>
/// <returns></returns>
protected GooglePaymentRequest GetBaseRequest() =>
    new GooglePaymentRequest
    {
        ApiVersion = 2,
        ApiVersionMinor = 0,
        TransactionInfo = new TransactionInfo
        {
            TotalPrice = "0.00",
            TotalPriceStatus = TotalPriceStatusTypes.Estimated,
            CurrencyCode = "GBP",
            CountryCode = "GB"
        },
        MerchantInfo = new MerchantInfo
        {
            MerchantName = ConfigurationManager.GlobalSettings.Wallets.GooglePay.MerchantName,
            MerchantId = ConfigurationManager.GlobalSettings.Wallets.GooglePay.MerchantIdentifier
        },
        AllowedPaymentMethods = new[]
        {
            new PaymentMethod
            {
                Type = GooglePaymentMethodType.Card,
                Parameters = new PaymentParameters
                {
                    AllowedAuthMethods = new[] { GoogleAuthMethod.PanOnly, GoogleAuthMethod.Cryptogram3ds },
                    AllowedCardNetworks = new[] { GoogleCardNetwork.Amex, GoogleCardNetwork.Interac, GoogleCardNetwork.Discover, GoogleCardNetwork.Mastercard, GoogleCardNetwork.Visa }
                },
                TokenizationSpecification = new TokenizationSpecification
                {
                    Type = GoogleTokenizationType.PaymentGateway,
                    Parameters = new TokenizationSpecificationParameters
                    {
                        Gateway = ConfigurationManager.GlobalSettings.Stripe.Gateway,
                        StripeVersion = ConfigurationManager.GlobalSettings.Stripe.StripeVersion,
                        StripeKey = ConfigurationManager.GlobalSettings.Stripe.StripePublishableKey
                    }
                }
            }
        }
    };

protected PaymentDataRequest CreatePaymentDataRequest(double total, string currencyCode)
{
    var request = GetBaseRequest();

    request.TransactionInfo = new TransactionInfo
    {
        TotalPrice = total.ToString("F"),
        TotalPriceStatus = TotalPriceStatusTypes.Final,
        CurrencyCode = LocaleHelper.GetRegionInfoByCurrencyCode(currencyCode).CurrencyCode,
        CountryCode = LocaleHelper.GetRegionInfoByCurrencyCode(currencyCode).CountryCode
    };

    var json = JsonConvert.SerializeObject(request, Formatting.None);
    Debug.WriteLine($"PaymentDataRequest: {json}");
    return PaymentDataRequest.FromJson(json);
}

// Replace with AddOnCompleteListener
private void OnAuthorizationComplete(object sender, IntentMessage intentMessage)
{
    try
    {
        var paymentData = PaymentData.GetFromIntent(intentMessage.Intent);
        string paymentInfo = paymentData.ToJson();

        if (paymentInfo == null)
        {
            return;
        }

        var paymentMethodData = (JObject)JsonConvert.DeserializeObject(paymentInfo);
        string tokenData = paymentMethodData.SelectToken("paymentMethodData.tokenizationData.token").ToString();
        var token = JsonConvert.DeserializeObject<GooglePaymentResponseToken>(tokenData);

        AuthorizationComplete.Invoke(token.Id);
    }
    catch (Exception ex)
    {

#if !DEBUG
_logger.LogError(ex, ex.Message);
#else
System.Diagnostics.Debug.WriteLine(ex.Message);
#endif
return;
}
}

public void HandlePaymentResult(ActivityResult result)
{
    if (result.ResultCode == (int)Result.Ok && result.Data != null)
    {
        var paymentData = PaymentData.GetFromIntent(result.Data);
        HandlePaymentSuccess(paymentData);
    }
    else
    {
        AuthorizationFailed?.Invoke();
    }
}

private void HandlePaymentSuccess(PaymentData paymentData)
{
    string paymentInfo = paymentData?.ToJson();
    if (!string.IsNullOrEmpty(paymentInfo))
    {
        var paymentMethodData = (JObject)JsonConvert.DeserializeObject(paymentInfo);
        string tokenData = paymentMethodData?.SelectToken("paymentMethodData.tokenizationData.token")?.ToString();

        AuthorizationComplete?.Invoke(tokenData);
    }
    else
    {
        AuthorizationFailed?.Invoke();
    }
}

}

public class IntentMessage : ValueChangedMessage
{
public IntentMessage(Intent intent) : base(intent)
{
Intent = intent;
}

public Intent Intent { get; set; }

}

public class ActivityResultCallback : Java.Lang.Object, IActivityResultCallback
{
private readonly Action _callback;

public ActivityResultCallback(Action<ActivityResult> callback)
{
    _callback = callback;
}

public void OnActivityResult(Java.Lang.Object result)
{
    if (result is ActivityResult activityResult)
    {
        _callback(activityResult);
    }
}

}

public class OnSuccessListener : Java.Lang.Object, IOnSuccessListener
{
private readonly Action _onSuccessAction;

public OnSuccessListener(Action<object> onSuccessAction)
{
    _onSuccessAction = onSuccessAction;
}

public void OnSuccess(Java.Lang.Object result)
{
    _onSuccessAction?.Invoke(result);
}

}

public class OnCompleteListener : Java.Lang.Object, IOnCompleteListener
{
private readonly Action _onComplete;

public OnCompleteListener(Action<Task> onComplete)
{
    _onComplete = onComplete;
}

public void OnComplete(Task task)
{
    _onComplete?.Invoke(task);
}

}

using System;
using Newtonsoft.Json;

namespace projectA.Client.Maui;

public class GooglePaymentRequest
{
[JsonProperty("apiVersion")]
public int ApiVersion { get; set; }
[JsonProperty("apiVersionMinor")]
public int ApiVersionMinor { get; set; }
[JsonProperty("merchantInfo")]
public MerchantInfo MerchantInfo { get; set; }
[JsonProperty("allowedPaymentMethods")]
public PaymentMethod[] AllowedPaymentMethods { get; set; }
[JsonProperty("transactionInfo")]
public TransactionInfo TransactionInfo { get; set; }
}

public class MerchantInfo
{
[JsonProperty("merchantName")]
public string MerchantName { get; set; }
[JsonProperty("merchantId")]
public string MerchantId { get; set; }
}

using System;
using Newtonsoft.Json;

namespace projectA.Client.Maui;

public class GooglePaymentResponseToken
{
public string Id { get; set; }
public string Object { get; set; }
[JsonProperty("client_ip")]
public string ClientIp { get; set; }
public int Created { get; set; }
public bool LiveMode { get; set; }
public string Type { get; set; }
public bool Used { get; set; }
}

public class TransactionInfo
{
[JsonProperty("totalPriceStatus")]
public string TotalPriceStatus { get; set; }
[JsonProperty("totalPrice")]
public string TotalPrice { get; set; }
[JsonProperty("countryCode")]
public string CountryCode { get; set; }
[JsonProperty("currencyCode")]
public string CurrencyCode { get; set; }
}

public class PaymentMethod
{
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("parameters")]
public PaymentParameters Parameters { get; set; }
[JsonProperty("tokenizationSpecification")]
public TokenizationSpecification TokenizationSpecification { get; set; }
}

public class PaymentParameters
{
[JsonProperty("allowedAuthMethods")]
public string[] AllowedAuthMethods { get; set; }
[JsonProperty("allowedCardNetworks")]
public string[] AllowedCardNetworks { get; set; }
}

public class TokenizationSpecification
{
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("parameters")]
public TokenizationSpecificationParameters Parameters { get; set; }
}

public class TokenizationSpecificationParameters
{
[JsonProperty("gateway")]
public string Gateway { get; set; }
[JsonProperty("stripe:version")]
public string StripeVersion { get; set; }
[JsonProperty("stripe:publishableKey")]
public string StripeKey { get; set; }
}

public static class GooglePaymentMethodType
{
public const string Card = "CARD"; // Card payment method
public const string TokenizedCard = "TOKENIZED_CARD"; // Tokenized card payment method
}

public static class TotalPriceStatusTypes
{
public const string Final = "FINAL"; // Final price, ready for processing
public const string Estimated = "ESTIMATED"; // Estimated price
}

public static class GoogleCardNetwork
{
public const string Visa = "VISA";
public const string Mastercard = "MASTERCARD";
public const string Amex = "AMEX";
public const string Discover = "DISCOVER";
public const string Jcb = "JCB";
public const string Interac = "INTERAC";
}

public static class GoogleAuthMethod
{
public const string PanOnly = "PAN_ONLY"; // Plain card number (PAN)
public const string Cryptogram3ds = "CRYPTOGRAM_3DS"; // 3D Secure cryptogram authentication
}

public static class GoogleTokenizationType
{
public const string PaymentGateway = "PAYMENT_GATEWAY"; // Use a payment gateway for tokenization
public const string Direct = "DIRECT"; // Direct integration for tokenization
}

Google button handler:
using Android.Content;
using Android.Gms.Wallet.Button;
using Android.Views;
using Android.Widget;
using projectA.Client.Maui.Controls.Buttons;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform;
using Android.Util;
using Microsoft.Maui.Controls.Compatibility.Platform.Android;
using Newtonsoft.Json.Linq;
using Android.Service.Controls;

namespace projectA.Client.Maui
{
public class PaymentButtonHandler : ViewHandler<PaymentButton, LinearLayout>
{
public PaymentButtonHandler() : base(PaymentButtonMapper) { }

    public static PropertyMapper<PaymentButton, PaymentButtonHandler> PaymentButtonMapper = new PropertyMapper<PaymentButton, PaymentButtonHandler>(ViewHandler.ViewMapper)
    {
        [nameof(PaymentButton.Command)] = MapCommand,
        [nameof(PaymentButton.CommandParameter)] = MapCommandParameter,
        //[nameof(PaymentButton.BackgroundColor)] = MapBackgroundColor,
        [nameof(PaymentButton.HeightRequest)] = MapHeightRequest,
        [nameof(PaymentButton.WidthRequest)] = MapWidthRequest,
    };

    protected override LinearLayout CreatePlatformView()
    {
        var context = Context ?? throw new InvalidOperationException("Context cannot be null");

        // Create a container (LinearLayout) to hold the PayButton and custom icon
        var container = new LinearLayout(context)
        {
            LayoutParameters = new LinearLayout.LayoutParams(
                ViewGroup.LayoutParams.MatchParent,
                ViewGroup.LayoutParams.WrapContent),
            Orientation = Orientation.Horizontal
        };

        // Create the Google Pay button
        var payButton = new PayButton(context)
        {
            LayoutParameters = new LinearLayout.LayoutParams(
                ViewGroup.LayoutParams.WrapContent,
                ViewGroup.LayoutParams.WrapContent)
            
        };

        // Optional: Customize the layout parameters
        var layoutParams = (LinearLayout.LayoutParams)payButton.LayoutParameters;
        layoutParams.SetMargins(16, 16, 16, 16);
        layoutParams.Height = 48;
        layoutParams.Weight = 0.5f;
        payButton.LayoutParameters = layoutParams;

        // Create a list of allowed payment methods (e.g., credit cards)
        var paymentMethods = new JArray
        {
            "CARD", // Card payment method (You can also use other methods such as "PAYPAL", etc.)
            //"BANK_ACCOUNT" // You can add other payment types here
        };

        // Initialize the button with Google Pay options
        payButton.Initialize(ButtonOptions.NewBuilder()
            .SetButtonTheme(ButtonConstants.ButtonTheme.Dark)
            .SetButtonType(ButtonConstants.ButtonType.Pay) // Adjust based on use case
            .SetCornerRadius(100)
            .SetAllowedPaymentMethods(paymentMethods.ToString())
            .Build());

        // Attach a click event handler
        payButton.Click += OnPayButtonClick;

        container.AddView(payButton);

        return container;
    }

    private void OnPayButtonClick(object? sender, EventArgs e)
    {
        try
        {
            // Trigger the Command bound to the Virtual View
            if (VirtualView?.Command != null && VirtualView.Command.CanExecute(VirtualView.CommandParameter))
            {
                VirtualView.Command.Execute(VirtualView.CommandParameter);
            }
        }
        catch (Exception ex)
        {
            // Log or handle exceptions
            Console.WriteLine($"Error in PayButton click handler: {ex.Message}");
        }
    }

    protected override void DisconnectHandler(LinearLayout platformView)
    {
        platformView.Click -= OnPayButtonClick;
        base.DisconnectHandler(platformView);
    }

    public static void MapCommand(PaymentButtonHandler handler, PaymentButton view)
    {
        if (handler.PlatformView != null)
        {
            handler.PlatformView.Click -= handler.OnPayButtonClick;
            if (view.Command != null)
            {
                handler.PlatformView.Click += handler.OnPayButtonClick;
            }
        }
    }

    public static void MapCommandParameter(PaymentButtonHandler handler, PaymentButton view)
    {
        // Handle mapping for CommandParameter
        // No direct API for parameters on PayButton, but can use VirtualView.CommandParameter
    }

    public static void MapText(PaymentButtonHandler handler, PaymentButton view)
    {
        // handler.PlatformView.Text = view.Text;
        // handler.PlatformView?.SetSelection(handler.PlatformView?.Text?.Length ?? 0);
    }

    public static void MapBackgroundColor(PaymentButtonHandler handler, PaymentButton view)
    {
        // handler.PlatformView?.SetTextColor(view.TextColor.ToPlatform());
        handler.PlatformView?.SetBackgroundColor(view.BackgroundColor.ToPlatform());
    }

    public static void MapHeightRequest(PaymentButtonHandler handler, PaymentButton view)
    {
        if (handler.PlatformView?.LayoutParameters is LinearLayout.LayoutParams layoutParams)
        {
            var context = handler.Context ?? throw new InvalidOperationException("Context cannot be null");

            // Convert HeightRequest to pixels
            var heightInPixels = view.HeightRequest > 0
                ? ToPixel(view.HeightRequest, context)
                : ViewGroup.LayoutParams.WrapContent;

            layoutParams.Height = heightInPixels;
            handler.PlatformView.LayoutParameters = layoutParams;
        }
    }

    public static void MapWidthRequest(PaymentButtonHandler handler, PaymentButton view)
    {
        if (handler.PlatformView?.LayoutParameters is LinearLayout.LayoutParams layoutParams)
        {
            var context = handler.Context ?? throw new InvalidOperationException("Context cannot be null");

            // Convert WidthRequest to pixels
            var widthInPixels = view.WidthRequest > 0
                ? ToPixel(view.HeightRequest, context)
                : ViewGroup.LayoutParams.WrapContent;

            layoutParams.Width = widthInPixels;
            handler.PlatformView.LayoutParameters = layoutParams;
        }
    }

    private static int ToPixel(double value, Context context)
    {
        var density = context.Resources.DisplayMetrics.Density;
        return (int)(value * density);
    }
}

}

@jfversluis first, I’d like to express my appreciation for all the hard work you have put into making .NET MAUI a robust and versatile framework.

I wanted to kindly ask whether there are any plans to introduce native support for Apple Pay and Google Pay in .NET MAUI. Many developers, including myself, would greatly benefit from having seamless payment integrations, as this would enhance the user experience and expand the capabilities of applications built on the framework.

If this is already on the roadmap, I would love to know if there is an estimated timeline. If not, could you please consider adding it as a feature request? I believe it would be a valuable addition to the ecosystem.

Thank you for your time and consideration. I look forward to your response!

@jfversluis
Copy link
Member

Thanks for all the investigation here! Good luck figuring out what NuGet it was and how to work around it :)

I remember there being an issue for the Apple Pay/Google Wallet API but I can't seem to find it. Honestly, I don't think we will be implementing that anytime soon. There is still lots of other things we want to do and primarily .NET MAUI is a UI framework and this is more of a non-UI thing.

If you can't find the Apple Pay/Google Wallet issue either, you could consider opening a new feature request for it. Great input for that could be this old Xamarin.Forms issue. Please don't just copy/paste it, but make sure its updated for the latest and greatest and how you envision it to work.

Thanks!

Gerald

@jfversluis jfversluis closed this as not planned Won't fix, can't repro, duplicate, stale Jan 31, 2025
@ikeoriaku123
Copy link
Author

No worries. I will consider opening a new feature request for it. I will also make it's updated. Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
platform/android 🤖 platform/iOS 🍎 platform/macOS 🍏 macOS / Mac Catalyst potential-regression This issue described a possible regression on a currently supported version., verification pending s/needs-attention Issue has more information and needs another look s/triaged Issue has been reviewed t/bug Something isn't working
Projects
None yet
Development

No branches or pull requests

4 participants