diff --git a/pay/example/ios/Flutter/AppFrameworkInfo.plist b/pay/example/ios/Flutter/AppFrameworkInfo.plist
index 4f8d4d24..8c6e5614 100644
--- a/pay/example/ios/Flutter/AppFrameworkInfo.plist
+++ b/pay/example/ios/Flutter/AppFrameworkInfo.plist
@@ -21,6 +21,6 @@
CFBundleVersion
1.0
MinimumOSVersion
- 11.0
+ 12.0
diff --git a/pay/example/ios/Podfile b/pay/example/ios/Podfile
index 88359b22..279576f3 100644
--- a/pay/example/ios/Podfile
+++ b/pay/example/ios/Podfile
@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
-# platform :ios, '11.0'
+# platform :ios, '12.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
diff --git a/pay/example/ios/Podfile.lock b/pay/example/ios/Podfile.lock
index 33c5b2fa..5e87ce2e 100644
--- a/pay/example/ios/Podfile.lock
+++ b/pay/example/ios/Podfile.lock
@@ -19,10 +19,10 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/pay_ios/ios"
SPEC CHECKSUMS:
- Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
- integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5
+ Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
+ integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
pay_ios: 8c7beb9c61d885f3f51b61f75f8793023fc8843a
-PODFILE CHECKSUM: fc81e398f362bae88bdf55239bd5cf842faad39f
+PODFILE CHECKSUM: c4c93c5f6502fe2754f48404d3594bf779584011
-COCOAPODS: 1.11.3
+COCOAPODS: 1.15.2
diff --git a/pay/example/ios/Runner.xcodeproj/project.pbxproj b/pay/example/ios/Runner.xcodeproj/project.pbxproj
index 20ee7217..b1d1dbe0 100644
--- a/pay/example/ios/Runner.xcodeproj/project.pbxproj
+++ b/pay/example/ios/Runner.xcodeproj/project.pbxproj
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
- objectVersion = 51;
+ objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
@@ -170,7 +170,7 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
- LastUpgradeCheck = 1300;
+ LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
@@ -237,10 +237,12 @@
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
+ "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
@@ -268,6 +270,7 @@
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
diff --git a/pay/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/pay/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index c87d15a3..5e31d3d3 100644
--- a/pay/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/pay/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -1,6 +1,6 @@
CADisableMinimumFrameDurationOnPhone
+ UIApplicationSupportsIndirectInputEvents
+
diff --git a/pay/example/lib/main.dart b/pay/example/lib/main.dart
index cc5710d3..7b7e8e62 100644
--- a/pay/example/lib/main.dart
+++ b/pay/example/lib/main.dart
@@ -35,18 +35,19 @@ class PayMaterialApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return const MaterialApp(
+ return MaterialApp(
title: 'Pay for Flutter Demo',
- localizationsDelegates: [
+ localizationsDelegates: const [
...GlobalMaterialLocalizations.delegates,
GlobalWidgetsLocalizations.delegate,
],
- supportedLocales: [
+ supportedLocales: const [
Locale('en', ''),
Locale('es', ''),
Locale('de', ''),
],
- home: PaySampleApp(),
+ theme: ThemeData.light(),
+ home: const PaySampleApp(),
);
}
}
@@ -64,16 +65,23 @@ class _PaySampleAppState extends State {
@override
void initState() {
super.initState();
- _googlePayConfigFuture =
- PaymentConfiguration.fromAsset('default_google_pay_config.json');
+ _googlePayConfigFuture = PaymentConfiguration.fromAsset(
+ 'default_google_pay_config.json',
+ );
}
void onGooglePayResult(paymentResult) {
debugPrint(paymentResult.toString());
}
- void onApplePayResult(paymentResult) {
+ void onApplePayResult(paymentResult, ApplePaymentConfirmation handler) async {
debugPrint(paymentResult.toString());
+
+ // This is where the payment result is fetched from the backend, and the
+ // payment result is updated accordingly.
+ bool isPaymentSuccessfull = true;
+
+ await handler.updatePaymentResult(isPaymentSuccessfull);
}
@override
@@ -128,23 +136,25 @@ class _PaySampleAppState extends State {
),
// Example pay button configured using an asset
FutureBuilder(
- future: _googlePayConfigFuture,
- builder: (context, snapshot) => snapshot.hasData
- ? GooglePayButton(
- paymentConfiguration: snapshot.data!,
- paymentItems: _paymentItems,
- type: GooglePayButtonType.buy,
- margin: const EdgeInsets.only(top: 15.0),
- onPaymentResult: onGooglePayResult,
- loadingIndicator: const Center(
- child: CircularProgressIndicator(),
- ),
- )
- : const SizedBox.shrink()),
+ future: _googlePayConfigFuture,
+ builder: (context, snapshot) => snapshot.hasData
+ ? GooglePayButton(
+ paymentConfiguration: snapshot.data!,
+ paymentItems: _paymentItems,
+ type: GooglePayButtonType.buy,
+ margin: const EdgeInsets.only(top: 15.0),
+ onPaymentResult: onGooglePayResult,
+ loadingIndicator: const Center(
+ child: CircularProgressIndicator(),
+ ),
+ )
+ : const SizedBox.shrink(),
+ ),
// Example pay button configured using a string
ApplePayButton(
paymentConfiguration: PaymentConfiguration.fromJsonString(
- payment_configurations.defaultApplePay),
+ payment_configurations.defaultApplePay,
+ ),
paymentItems: _paymentItems,
style: ApplePayButtonStyle.black,
type: ApplePayButtonType.buy,
diff --git a/pay/lib/src/pay.dart b/pay/lib/src/pay.dart
index 97cc75fd..64bf2e61 100644
--- a/pay/lib/src/pay.dart
+++ b/pay/lib/src/pay.dart
@@ -40,7 +40,10 @@ class Pay {
/// Creates an instance with a dictionary of [_configurations] and
/// instantiates the [_payPlatform] to communicate with the native platforms.
- Pay(this._configurations) : _payPlatform = PayMethodChannel();
+ Pay(this._configurations)
+ : _payPlatform = defaultTargetPlatform == TargetPlatform.iOS
+ ? IosPayMethodChannel()
+ : PayMethodChannel();
/// Determines whether a user can pay with the selected [provider].
///
@@ -70,6 +73,19 @@ class Pay {
_configurations[provider]!, paymentItems);
}
+ /// Update the payment result with the native platform.
+ /// Works only on iOS.
+ Future updatePaymentResult(bool isSuccess) async {
+ if (_payPlatform is IosPayMethodChannel) {
+ final iosPayPlatform = _payPlatform as IosPayMethodChannel;
+ return iosPayPlatform.updatePaymentResult(isSuccess);
+ } else {
+ throw MethodNotForCurrentPlatformException(
+ 'The method "updatePaymentResult" can only be called when the "defaultTargetPlatform" is iOS.',
+ );
+ }
+ }
+
/// Verifies that the selected provider has been previously configured or
/// throws otherwise.
Future throwIfProviderIsNotDefined(PayProvider provider) async {
@@ -91,3 +107,15 @@ class ProviderNotConfiguredException implements Exception {
@override
String toString() => 'ProviderNotConfiguredException: $message';
}
+
+/// Thrown to indicate that the method called is not available for the current
+/// platform is has been called from.
+class MethodNotForCurrentPlatformException implements Exception {
+ MethodNotForCurrentPlatformException(this.message);
+
+ /// A human-readable error message, possibly null.
+ final String? message;
+
+ @override
+ String toString() => 'MethodNotForCurrentPlatformException: $message';
+}
diff --git a/pay/lib/src/widgets/apple_pay_button.dart b/pay/lib/src/widgets/apple_pay_button.dart
index 2016667d..6355e76f 100644
--- a/pay/lib/src/widgets/apple_pay_button.dart
+++ b/pay/lib/src/widgets/apple_pay_button.dart
@@ -14,6 +14,18 @@
part of '../../pay.dart';
+class ApplePaymentConfirmation {
+ final Pay _payClient;
+ ApplePaymentConfirmation(Pay payClient) : _payClient = payClient;
+
+ Future updatePaymentResult(bool completedSuccessfully) async {
+ await _payClient.updatePaymentResult(completedSuccessfully);
+ }
+}
+
+typedef ApplePaymentConfirmCallback = Function(
+ Map result, ApplePaymentConfirmation handler);
+
/// A widget to show the Apple Pay button according to the rules and constraints
/// specified in [PayButton].
///
@@ -38,7 +50,7 @@ class ApplePayButton extends PayButton {
super.key,
super.buttonProvider = PayProvider.apple_pay,
required super.paymentConfiguration,
- super.onPaymentResult,
+ required ApplePaymentConfirmCallback onPaymentResult,
required List paymentItems,
double? cornerRadius,
ApplePayButtonStyle style = ApplePayButtonStyle.black,
@@ -51,7 +63,11 @@ class ApplePayButton extends PayButton {
super.childOnError,
super.loadingIndicator,
}) : assert(width >= RawApplePayButton.minimumButtonWidth),
- assert(height >= RawApplePayButton.minimumButtonHeight) {
+ assert(height >= RawApplePayButton.minimumButtonHeight),
+ super(paymentCallback: (result) {
+ final pay = Pay({buttonProvider: paymentConfiguration});
+ onPaymentResult(result, ApplePaymentConfirmation(pay));
+ }) {
_applePayButton = RawApplePayButton(
style: style,
type: type,
diff --git a/pay/lib/src/widgets/google_pay_button.dart b/pay/lib/src/widgets/google_pay_button.dart
index ec273662..2031390b 100644
--- a/pay/lib/src/widgets/google_pay_button.dart
+++ b/pay/lib/src/widgets/google_pay_button.dart
@@ -14,6 +14,8 @@
part of '../../pay.dart';
+typedef GooglePaymentCallback = Function(Map result);
+
/// A widget to show the Google Pay button according to the rules and
/// constraints specified in [PayButton].
///
@@ -38,7 +40,7 @@ class GooglePayButton extends PayButton {
super.key,
super.buttonProvider = PayProvider.google_pay,
required final PaymentConfiguration paymentConfiguration,
- super.onPaymentResult,
+ required GooglePaymentCallback onPaymentResult,
required List paymentItems,
int cornerRadius = RawGooglePayButton.defaultButtonHeight ~/ 2,
GooglePayButtonTheme theme = GooglePayButtonTheme.dark,
@@ -52,7 +54,12 @@ class GooglePayButton extends PayButton {
super.loadingIndicator,
}) : assert(width >= RawGooglePayButton.minimumButtonWidth),
assert(height >= RawGooglePayButton.defaultButtonHeight),
- super(paymentConfiguration: paymentConfiguration) {
+ super(
+ paymentConfiguration: paymentConfiguration,
+ paymentCallback: (result) {
+ onPaymentResult(result);
+ },
+ ) {
_googlePayButton = RawGooglePayButton(
paymentConfiguration: paymentConfiguration,
cornerRadius: cornerRadius,
diff --git a/pay/lib/src/widgets/pay_button.dart b/pay/lib/src/widgets/pay_button.dart
index 55337e9c..1b3264ca 100644
--- a/pay/lib/src/widgets/pay_button.dart
+++ b/pay/lib/src/widgets/pay_button.dart
@@ -14,6 +14,8 @@
part of '../../pay.dart';
+typedef PaymentResultCallback = void Function(Map result);
+
/// A widget that handles the API logic to facilitate the integration.
///
/// This widget provides an alternative UI-based integration path that wraps
@@ -27,13 +29,13 @@ part of '../../pay.dart';
/// method which starts the payment process.
abstract class PayButton extends StatefulWidget {
/// A resident client to issue requests against the APIs.
- late final Pay _payClient;
+ final Pay _payClient;
/// Specifies the payment provider supported by the button
final PayProvider buttonProvider;
/// A function called when the payment process yields a result.
- final void Function(Map result)? onPaymentResult;
+ final PaymentResultCallback? paymentCallback;
final double width;
final double height;
@@ -56,7 +58,7 @@ abstract class PayButton extends StatefulWidget {
super.key,
required this.buttonProvider,
required final PaymentConfiguration paymentConfiguration,
- this.onPaymentResult,
+ this.paymentCallback,
this.width = 0,
this.height = 0,
this.margin = const EdgeInsets.all(0),
@@ -77,7 +79,7 @@ abstract class PayButton extends StatefulWidget {
try {
final result =
await _payClient.showPaymentSelector(buttonProvider, paymentItems);
- onPaymentResult?.call(result);
+ paymentCallback?.call(result);
} catch (error) {
onError?.call(error);
}
diff --git a/pay_ios/ios/Classes/PayPlugin.swift b/pay_ios/ios/Classes/PayPlugin.swift
index 51da04c1..2b8500d3 100644
--- a/pay_ios/ios/Classes/PayPlugin.swift
+++ b/pay_ios/ios/Classes/PayPlugin.swift
@@ -23,6 +23,7 @@ public class PayPlugin: NSObject, FlutterPlugin {
private static let methodChannelName = "plugins.flutter.io/pay_channel"
private let methodUserCanPay = "userCanPay"
private let methodShowPaymentSelector = "showPaymentSelector"
+ private let methodUpdatePaymentResult = "updatePaymentResult"
private let paymentHandler = PaymentHandler()
@@ -48,6 +49,10 @@ public class PayPlugin: NSObject, FlutterPlugin {
paymentConfiguration: arguments["payment_profile"] as! String,
paymentItems: arguments["payment_items"] as! [[String: Any?]])
+ case methodUpdatePaymentResult:
+ let isSuccess = call.arguments as! Bool
+ paymentHandler.updatePaymentResult(isSuccess: isSuccess)
+
default:
result(FlutterMethodNotImplemented)
}
diff --git a/pay_ios/ios/Classes/PaymentHandler.swift b/pay_ios/ios/Classes/PaymentHandler.swift
index 887aadf0..ea191378 100644
--- a/pay_ios/ios/Classes/PaymentHandler.swift
+++ b/pay_ios/ios/Classes/PaymentHandler.swift
@@ -36,6 +36,9 @@ enum PaymentHandlerStatus {
/// paymentHandler.canMakePayments(stringArguments)
/// ```
class PaymentHandler: NSObject {
+
+ /// Holds the completion handler so it can be updated later on from the Flutter side.
+ var completionHandler: PaymentCompletionHandler?
/// Holds the current status of the payment process.
var paymentHandlerStatus: PaymentHandlerStatus!
@@ -106,7 +109,7 @@ class PaymentHandler: NSObject {
///
/// - parameter paymentConfigurationString: A JSON string with the configuration to execute
/// this payment.
- /// - returns: A list of recognized networks supported for this operation.
+ /// - returns: A list of recognized networks supported for this operation.
private static func supportedNetworks(from paymentConfigurationString: String) -> [PKPaymentNetwork]? {
guard let paymentConfiguration = extractPaymentConfiguration(from: paymentConfigurationString) else {
return nil
@@ -169,6 +172,16 @@ class PaymentHandler: NSObject {
return paymentRequest
}
+
+ /// Updates the payment result based on the value you pass to the `isSuccess` parameter.
+ ///
+ /// - parameter isSuccess: A boolean that determines whether the payment was successful or not.
+ /// - returns: nothing.
+ func updatePaymentResult(isSuccess: Bool) {
+ // Call completion handler with the given success status.
+ completionHandler?(isSuccess)
+ completionHandler = nil
+ }
}
/// Extension that implements the completion methods in the delegate to respond to user selection.
@@ -179,7 +192,6 @@ extension PaymentHandler: PKPaymentAuthorizationControllerDelegate {
}
func paymentAuthorizationController(_: PKPaymentAuthorizationController, didAuthorizePayment payment: PKPayment, handler completion: @escaping (PKPaymentAuthorizationResult) -> Void) {
-
// Collect payment result or error and return if no payment was selected
guard let paymentResultData = try? JSONSerialization.data(withJSONObject: payment.toDictionary()) else {
self.paymentResult(FlutterError(code: "paymentResultDeserializationFailed", message: nil, details: nil))
@@ -189,8 +201,13 @@ extension PaymentHandler: PKPaymentAuthorizationControllerDelegate {
// Return the result back to the channel
self.paymentResult(String(decoding: paymentResultData, as: UTF8.self))
+ // Store completion handler
+ completionHandler = { isSuccess in
+ let status: PKPaymentAuthorizationStatus = isSuccess ? .success : .failure
+ completion(PKPaymentAuthorizationResult(status: status, errors: nil))
+ }
+
paymentHandlerStatus = .authorized
- completion(PKPaymentAuthorizationResult(status: PKPaymentAuthorizationStatus.success, errors: nil))
}
func paymentAuthorizationControllerDidFinish(_ controller: PKPaymentAuthorizationController) {
diff --git a/pay_ios/lib/pay_ios.dart b/pay_ios/lib/pay_ios.dart
index 487252c6..6f4b324f 100644
--- a/pay_ios/lib/pay_ios.dart
+++ b/pay_ios/lib/pay_ios.dart
@@ -16,4 +16,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
+export 'src/ios_pay_channel.dart';
+
part 'src/widgets/apple_pay_button.dart';
diff --git a/pay_ios/lib/src/ios_pay_channel.dart b/pay_ios/lib/src/ios_pay_channel.dart
new file mode 100644
index 00000000..320e9be9
--- /dev/null
+++ b/pay_ios/lib/src/ios_pay_channel.dart
@@ -0,0 +1,13 @@
+/// Copyright 2023 Google LLC.
+/// SPDX-License-Identifier: Apache-2.0
+library;
+
+import 'package:pay_platform_interface/pay_channel.dart';
+
+/// This implements the iOS specific functionality of the Pay plugin.
+class IosPayMethodChannel extends PayMethodChannel {
+ /// Update the payment result with the native platform.
+ Future updatePaymentResult(bool isSuccess) async {
+ return channel.invokeMethod('updatePaymentResult', isSuccess);
+ }
+}
diff --git a/pay_ios/test/src/ios_pay_channel_test.dart b/pay_ios/test/src/ios_pay_channel_test.dart
new file mode 100644
index 00000000..f0d57257
--- /dev/null
+++ b/pay_ios/test/src/ios_pay_channel_test.dart
@@ -0,0 +1,68 @@
+/// Copyright 2023 Google LLC
+///
+/// Licensed under the Apache License, Version 2.0 (the "License");
+/// you may not use this file except in compliance with the License.
+/// You may obtain a copy of the License at
+///
+/// https://www.apache.org/licenses/LICENSE-2.0
+///
+/// Unless required by applicable law or agreed to in writing, software
+/// distributed under the License is distributed on an "AS IS" BASIS,
+/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+/// See the License for the specific language governing permissions and
+/// limitations under the License.
+library;
+
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:pay_ios/pay_ios.dart';
+
+void main() {
+ TestWidgetsFlutterBinding.ensureInitialized();
+
+ late final IosPayMethodChannel payChannel;
+
+ final defaultBinaryMessenger =
+ TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
+
+ setUpAll(() async {
+ payChannel = IosPayMethodChannel();
+ });
+
+ group('Verify channel I/O for', () {
+ final log = [];
+ const testResponses = {
+ 'updatePaymentResult': null,
+ };
+
+ setUp(() {
+ defaultBinaryMessenger.setMockMethodCallHandler(
+ payChannel.channel,
+ (MethodCall methodCall) async {
+ log.add(methodCall);
+ final response = testResponses[methodCall.method];
+ if (response is Exception) {
+ return Future