Closed dengjeffrey closed 7 years ago
Below is my thought process and possible solution to implementing this. I have not experimented on Android thoroughly so any input will be helpful
@mikkoh
UnitySendMessage
.On a checkout screen we will make a call Cart.CanCheckoutWithNativePay() -> Bool
which determines if Apple Pay or Android Pay is available on the device. Based on the result the we will show a Native Pay button.
false
is returned on an iOS device, we can decide if we would like to support an integrated Apple Pay Setup using Cart.CanShowApplePaySetup() -> Bool
to determine if the device supports setting up a new card. If so we can display a setup Apple Pay Button and notify the Swift plugin when it is pressed to display a native setup.Once a Native Pay button is pressed, we will call Cart.PrepareNativeCheckout(string key, Action<WalletInfo> success, Action<CheckoutFailureType ,string> failure)
to continue the checkout process.
Action<WalletInfo> completion
is the callback for a successAction<CheckoutFailureType , string> failure)
is a callback for failure, the string
contains the error message, and CheckoutFailureType
is a enum.PrepareNativeCheckout
will be returned in a different method, the Action
s are stored in an instance variable to be invoked in a different function.
Action
because we will not encounter the case where their are multiple concurrent payment sessions, and this simplifies the implementation.key
is the Private Key for encrypting and decrypting Wallet, supplied by the implementorAction<WalletInfo> success
, WalletInfo
will be set to the info provided in a MaskedWallet
PrepareNativeCheckout(...)
tells the Android Plugin to create a MaskedWalletRequest
and fetch a MaskedWallet
.
A native UI sheet appears on the screen after requesting to fetch a MaskedWallet
. This is where the user chooses a payment option and fills out their shipping address.
When the user dismisses the sheet, a MaskedWallet
is returned to an Android delegate method and the Android Plugin will pass a serialized MaskedWallet
to Unity via DidFetchMaskedWallet(string walletData)
or DidCancelCheckout()
We will display the MaskedWallet
info to the user so that they can confirm that the shipping address and payment card is correct.
WalletFragment
to facilitate modifications to the MaskedWallet
because we can not specify where to place a WalletFragment
without having to subclass UnityPlayerActivity
which is adding more restrictions for the implementor. Instead we will provide a hook in the Android Plugin to access changeMaskedWallet()
MaskedWallet
is stored in the Android Plugin for future requests when we create a FullWallet
// Unity
enum CheckoutFailureType {Cancelled, Error}
void PrepareNativeCheckout(string key, Action<WalletInfo> success, Action<CheckoutFailureType, string> failure) {
// Instance variables
onPrepareCheckoutSuccess = success;
onCheckoutFailure = failure;
#if UNITY_ANDROID
// _Function denotes a function exposed from our Android Plugin
// Android Plugin will create a MaskedWalletRequest and fetch a MaskedWallet
_FetchMaskedWallet(publicKey, Store.name, Store.currencyCode);
// When the Android Plugin recieves the MaskedWallet
// it will call DidFetchMaskedWallet
#endif
}
void DidFetchMaskedWallet(string walletData) {
// Parse walletData
WalletInfo info = WalletInfo(walletData);
onPrepareCheckoutSuccess(info);
}
void DidCancelCheckout() {
onCheckoutFailure(CheckoutFailureType.Cancelled, null);
}
void DidFailToFetchMaskedWallet(string error) {
onCheckoutFailure(CheckoutFailureType.Error, error);
}
Once the user has pressed the confirm button we will call CompleteNativeCheckout()
which tells the Android Plugin to create a FullWalletRequest
and fetch a FullWallet
MaskedWallet
The payment token from FullWallet
is passed to Unity where we will complete the token checkout
// Unity
void CompleteNativeCheckout(Action success, Action<CheckoutFailureType, string> failure) {
// Instance variables
onCompleteCheckoutSuccess = success;
onCheckoutFailure = failure;
#if UNITY_ANDROID
// _Function denotes a function exposed from our Android Plugin
// Android Plugin will create a MaskedWalletRequest and fetch a MaskedWallet
_FetchFullWallet();
// When the Android Plugin receives the FullWallet
// it will call DidFetchPaymentToken
#endif
}
void DidFetchFullWallet(string token) {
// Send token to server to be processed
CheckoutWithToken(token, (success, errorMessage) => {
if (success) {
onCompleteCheckoutSuccess();
}
else {
onCheckoutFailure(CheckoutFailureType.Error, errorMessage);
}
})
}
void DidCancelCheckout() {
onCheckoutFailure(CheckoutFailureType.Cancelled, null);
}
void DidFailToFetchMaskedWallet(string error) {
onCheckoutFailure(CheckoutFailureType.Error, error);
}
key
is the Merchant ID of the Apps, supplied by the implementorAction<WalletInfo> success
, WalletInfo
will be set to null
since there is no info received by iOS during this stepPrepareNativeCheckout (string merchantID)
consists of 3 sub steps handled by the Swift Plugin.
PKPaymentRequest
PKSummaryItem
from CartLineItem
and other Cart detailsPKPaymentAuthorizationViewController
on top of the current ViewController
// Unity
void PrepareNativeCheckout(string key, Action<WalletInfo> success, Action<CheckoutFailureType, string> failure) {
// Instance variables
onPrepareCheckoutSuccess = success;
onCheckoutFailure = failure;
#if UNITY_IOS
// _Function denotes a function exposed from a C# wrapper of ObjC/Swift methods
// Create a PKPaymentRequest
_CreateApplePaySession(key, Store.countryCode, Store.currencyCode, requiresBilling))
// Directly call completion, there is no point in catching failure.
// _CreateApplePaySession will crash on a failure.
// If it does crash then Store.countryCode and currency code are not ISO codes
// Which means there is an issue with the values returned by the server or
// our SDK parses the data incorrectly which is a fatal error
onPrepareCheckoutSuccess(null)
#endif
}
The implementor then calls CompleteNativeCheckout
// Unity
void CompleteNativeCheckout(Action success, Action<CheckoutFailureType, string> failure) {
// Instance variables
onCompleteCheckoutSuccess = success;
onCheckoutFailure = failure;
#if UNITY_IOS
// Create PKSummaryItems
_AddApplePaySummaryItem("Cart Total", Cart.total());
_AddApplePaySummaryItem("Shipping Rate", Cart.shippingRate());
_AddApplePaySummaryItem("Taxes", Cart.taxes());
...
// Create PKShippingItems
_AddApplePayShippingMethod("Free", "Takes 5-10 business days", Cart.ShippingRates[0].cost)
...
// Present PKPaymentAuthorizationViewController
_PresentApplePayAuthorization();
#endif
}
PKPaymentAuthorizationViewControllerDelegate
in the Swift Plugin.
// Swift Plugin
func paymentAuthorizationViewController(PKPaymentAuthorizationViewController,
didSelect shippingMethod: PKShippingMethod,
completion: (PKPaymentAuthorizationStatus, [PKPaymentSummaryItem]) -> Void) {
// Notify Unity of a change, this call is asynchronous
UnityWrapper.sendMessage(shippingMethod.identifier, toObject:"CartObject", withMethodName:"UpdateShippingForIdentifier")
DispatchQueue.global(qos: .userInitiated).async {
self.updateSemaphore.wait()
completion(self.summaryItems)
}
}
func didUpdateSummaryItems() {
updateSemaphore.signal()
}
// Unity
void UpdateSummaryItemsForShippingIdentifier(string identifier) {
// Update the summary items and notify Swift Plugin
_RemoveAllApplePaySummaryItems();
...
_AddApplePaySummaryItem("Total", 4.0f);
_DidUpdateApplePaySummaryItems();
}
PKAuthorizationViewController
is shown since Unity does not allow messages to be sent when the app has resigned active. Although Unity allows background fetch, communication between both ends are limited.
Subclass UnityAppController
, set _didResignActive = false
when the app notifies Unity that the App is resigning active due to displaying the PKAuthorizationController
extern bool _didResignActive;
IMPL_APP_CONTROLLER_SUBCLASS(BuyUnityAppController)
@interface BuyUnityAppController()
@end
@implementation BuyUnityAppController
- (id)init {
if (self = [super init]) {
_shouldResignActive = true;
}
return self;
}
- (void)applicationWillResignActive:(UIApplication*)application
{
[super applicationWillResignActive:application];
if (!_shouldResignActive) {
_didResignActive = false;
}
}
@davidmuzi @dbart01 do you guys mind checking this issue out? It's a proposal on how to implement Apple Pay and Android Pay into the Unity SDK.
I'm not sure there's any benefit (aside from syntax) in using Swift for Apple Pay implementation. Since all classes defined in Swift have to stay compatible with ObjC (can't use rawValue
backed enum
, can't use generics, etc), there's really not much benefit in using Swift compared to just Objective-C. What you do gain with ObjC though is direct compatibility with Objective-C++, just rename to .mm
and you're set. Whether the nice Swift syntax is worth the trade-off with C++ compatibility if your call but just something to keep in mind. It might eliminate some boiler-plate wrappers.
This looks pretty good! I think we still need to think about this. After reading this it’s still unclear to me how this would fully work. Here are some questions and comments I had when reading through this.
Cart.CanShowApplePaySetup()
didn’t even think of this. This is great. Can we do this for Android also. If we can we could do Cart.CanShowNativePaySetup()
publicKey
come from for void PrepareCheckoutWithAndroidPay(string publicKey) {
?void CompleteCheckoutWithToken(string message)
should it be void CompleteCheckoutWithToken(string token)
?merchantId
come from for void CheckoutWithApplePay(string merchantID) {
?_FetchMaskedWallet
bring up native ui? Is the user able to cancel this process? CompleteCheckoutWithToken
is called on success but is there a failure case?I still would like to explore if it’s possible to consolidate the steps so that the process of native checkout is similar. Could we do something like this?
cart.CanCheckoutWithNativePay()
cart.CanShowNativePaySetup()
cart.PrepareNativeCheckout(OnPrepareSucess, OnPrepareFailure)
(I know on iOS this would not do much but I'd rather keep the process consistent than efficient)cart.CompleteCheckoutWithToken(token, OnPrepareSucess, OnPrepareFailure)
I’m unsure of the decision to use Swift at this point. Mainly because I don’t know the mobile ecosystem well. It would be a huge bummer if a developer was attempting to incorporate the Unity SDK into a pre-existing published project and was not able to use Swift for some reason or another.
UnitySendMessage
from C++ to Swift. The positive trade off is we would be building for the long term, for if/when Swift has a higher adoption rate.Cart.CanShowApplePaySetup() didn’t even think of this. This is great. Can we do this for Android also. If we can we could do Cart.CanShowNativePaySetup()
Where does publicKey come from for void PrepareCheckoutWithAndroidPay(string publicKey) {?
publicKey
is provided by the implementor of the SDK. The key is used by Android to encrypt wallet information, and later on we use it to decrypt the FullWallet
in order to access the
Payment Token.For void CompleteCheckoutWithToken(string message) should it be void CompleteCheckoutWithToken(string token)?
void CompleteCheckoutWithToken(string token)
good catchWhere does merchantId come from for void CheckoutWithApplePay(string merchantID) {?
merchantId
is provided by the implementor of the SDK. When creating an app with Apple Pay capabilities, they must create a merchantId
from the Apple Developer Portal.When you state We will display the MaskedWallet info to the user so that they can confirm that the shipping address and payment card is correct. does this mean that we need to build UI that shows this info?
WalletFragment
which is a fragment provided by the Android API. If they choose to use a WalletFragment
then they would have to subclass or modify the behaviour of the UnityPlayerActivity
to display the WalletFragment
. I think we should leave this up to the implementor because they may already have subclassed the UnityPlayerActivity which may cause conflicts. This part of the flow is shown in step 3 of the Android Payment FlowDoes _FetchMaskedWallet bring up native ui? Is the user able to cancel this process? CompleteCheckoutWithToken is called on success but is there a failure case?
_FetchMaskedWallet
and _FetchFullWallet
will bring up native UI, and they can cancel it. If _FetchMaskedWallet
results in an error or if the user cancels, then this error message will be propagated to Unity via UnitySendMessage
where we can notify the implementor via a delegate message or invoke a UnityAction
closure to have the Implementor show a customized alert. We can also provide a default option and present a native toast similar to how Mobile Buy SDK - Android handles it: private void handleGoogleWalletError(int errorCode) {
switch (errorCode) {
// Recoverable Wallet errors
case WalletConstants.ERROR_CODE_SPENDING_LIMIT_EXCEEDED:
Log.e(LOG_TAG, "Spending limit exceeded");
Toast.makeText(this, "Spending Limit exceeded. Please adjust your cart and try again", Toast.LENGTH_SHORT).show();
launchProductListActivity();
break;
// Unrecoverable Wallet errors
case WalletConstants.ERROR_CODE_INVALID_PARAMETERS:
case WalletConstants.ERROR_CODE_AUTHENTICATION_FAILURE:
case WalletConstants.ERROR_CODE_BUYER_ACCOUNT_ERROR:
case WalletConstants.ERROR_CODE_MERCHANT_ACCOUNT_ERROR:
case WalletConstants.ERROR_CODE_SERVICE_UNAVAILABLE:
case WalletConstants.ERROR_CODE_UNSUPPORTED_API_VERSION:
case WalletConstants.ERROR_CODE_UNKNOWN:
default:
Log.e(LOG_TAG, "Unrecoverable wallet error:" + errorCode);
Toast.makeText(this, "Could not complete the checkout, please try again later", Toast.LENGTH_SHORT).show();
break;
}
}
PKPaymentAuthorizationViewController
. Since the Apple Pay flow requires that we complete the checkout by sending necessary payment data to the server before dismissing the PKPaymentAuthorizationViewController
we can fail on the call to checkout, which PKPaymentAuthorizationViewController
will handle by displaying the status message to the user.func paymentAuthorizationViewController(_ controller: PKPaymentAuthorizationViewController, didAuthorizePayment payment: PKPayment, completion: @escaping (PKPaymentAuthorizationStatus) -> Void) {
let authStatus = ... status from performing checkout
completion(authStatus)
}
I'll also add some of these comments above. I think you're right making it consistent is probably less confusing for the implementor. @mikkoh
I'll try to read over this comment again and give detailed feedback. But I just want to bring up one thing.
we can fail on the call to checkout, which PKPaymentAuthorizationViewController will handle by displaying the status message to the user
I think it would be good if we still gave the Unity developer the ability to be reactive to a failure. For instance lets say in their game the main character reacts to users decision to not buy product. For instance lets say they shake their fist towards the screen when the user doesnt buy the stuff their selling. Or gives a thumbs up after the purchase experience if they did buy something.
In order to facilitate this we need to add a callback for failures and successes.
@dengjeffrey any updates on this? You mentioned today that @dbart01 had given you some new ideas on implementation.
@mikkoh I will have a more detailed write up before noon tomorrow
@dengjeffrey here are some updated questions and me just trying to think all of this through.
CanShowNativePaySetup
instead of CanShowApplePaySetup
. Cart.CanShowNativePaySetup()
always returns false
for Android this means that the developer can write code once for all platforms.key
just generated? If so is there any value allowing the user to pass in their own key or do we just generate once. If we make this optional how would that effect iOS's method signature.PrepareNativeCheckout
what would the user use WalletInfo
for? What will be included in WalletInfo
? Let me know if this example usage makes sense:
if (Cart.CanCheckoutWithNativePay()) {
Cart.PrepareNativeCheckout(key, (walletInfo) => {
// what can I do with walletInfo ?!?
Cart.CompleteNativeCheckout(onCompleteSucess, onCompleteFail);
}, onPrepareFail);
} else if (Cart.CanShowNativePaySetup()) {
Cart.ShowNativePaySetup(onNativePaySetupSuccess, onNativePaySetupFail);
}
All methods on Cart for native checkout:
Cart.CanCheckoutWithNativePay();
Cart.CanShowNativePaySetup();
Cart.ShowNativePaySetup();
Cart.PrepareNativeCheckout(key, onPrepareSuccess, onPrepareFail);
Cart.CompleteNativeCheckout(onCompleteSucess, onCompleteFail);
Yes we can have CanShowNativePaySetup()
return false for Android, it makes the flow more simple but I hope it won't cause confusion.
key
is a public key from the implementor, this public key is generated on Shopify's server for the store. They get this key from the Mobile App sales channel on their store dashboard.
walletInfo contains the following:
String[] paymentDescriptions
MailingAddress shippingAddress
Below is what an example usage would look like.
if (Cart.CanCheckoutWithNativePay()) {
// Display Pay with Android or Apple Pay button, alongside web checkout button
#if UNITY_IOS
ApplePayButton.Instantiate(onClick: () => {
Cart.PrepareNativeCheckout(key, (walletInfo) => {
// Do nothing with walletInfo, since it's null on Apple Pay
Cart.CompleteNativeCheckout(onCompleteSucess, onCompleteFail);
}
})
#elif UNITY_ANDROID
AndroidPayButton.Instantiate(onClick: () => {
Cart.PrepareNativeCheckout(key, (walletInfo) => {
// Display a confirmation UI to the user based on the info WalletInfo, the implementor has to make this UI
// walletInfo contains the following:
// String[] paymentDescriptions (https://developers.google.com/android/reference/com/google/android/gms/wallet/MaskedWallet)
// MailingAddress shippingAddress
WalletConfirmationUI.Instantiate(onConfirmClick: () => {
Cart.CompleteNativeCheckout(onCompleteSucess, onCompleteFail);
})
}
})
#else
// Show some Unity editor button for testing
#endif
} else if (Cart.CanShowNativePaySetup()) {
// Display a Setup Apple Pay button.
// This part might cause confusion for the implementor
// whether or not the implementor will need a Button for Android,
// they will have to pay close attention to our Docs.
Cart.ShowNativePaySetup(onNativePaySetupSuccess, onNativePaySetupFail);
}
In addition to the methods you have mentioned in Cart for native checkout, we need to include Update methods. So far for iOS:
void UpdateSummaryItemsForShippingIdentifier(string identifier)
void UpdateSummaryItemsForShippingAddress(string addressJSON)
void UpdateSummaryItemsForShippingContact(string contactJSON)
void UpdateSummaryItemsForPaymentMethod(string paymentMethodJSON)
// This one is called when a payment token is generated via Apple Pay and
// Apple pay needs a server status whether the payment went through or not before dismissing
void FetchCheckoutStatusForToken(string token)
// Calls to Plugin
bool CanCheckoutWithNativePay()
bool CanShowNativePaySetup()
void ShowNativePaySetup()
void PrepareNativeCheckoutWithKey(string key, Action<WalletInfo> success, Action<CheckoutFailureType, string> failure)
void CompleteNativeCheckout(Action success, Action<CheckoutFailureType, string> failure)
What
Allow the SDK to support Apple Pay and Android Pay as part of the checkout flow.
How
Integrating native in app payments require the use of Native Plugins
UnitySendMessage("GameObjectName1", "MethodName1", "Message to send")
Checklist
PassKit
Objects going to UnityPassKit
Objects coming from UnityPaymentSession
that manages the Authorization ControllerApplePayEventDispatcher
that forwards events from Authorization Controller to UnityMessageCenter
that handles responses from UnityCart+ApplePay
a native C++ wrapper that exposes methods to create aPaymentSession
Cart
that allow the user to create aPaymentSession
ApplePayEventDispatcher
in Unity