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

➕ Add the success/failure handling from Dart side for Apple Pay #200

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a3b1b81
add the success/failure handling from dart for apple pay
YazeedAlKhalaf May 3, 2023
78dfe7b
enhance the example app Pay usage
YazeedAlKhalaf Jun 15, 2023
fcab163
move impl. of updatePaymentStatus to pay_ios and test it
YazeedAlKhalaf Aug 2, 2023
0d8c189
update the example app to showcase the new feature
YazeedAlKhalaf Aug 2, 2023
23b63f8
fix deprecated thing in a test
YazeedAlKhalaf Aug 2, 2023
cddc099
try to match the styling of the other fellow files
YazeedAlKhalaf Aug 2, 2023
1323120
remove dark theme from demo app
YazeedAlKhalaf Dec 20, 2023
4a48cc8
rename IOS to Ios in IOSPayMethodChannel
YazeedAlKhalaf Dec 20, 2023
a06e1ec
rename update payment status to update payment result
YazeedAlKhalaf Dec 20, 2023
cd07e7c
add docs for updatePaymentResult
YazeedAlKhalaf Dec 20, 2023
87268b8
ran the project using xcode 15
YazeedAlKhalaf Dec 20, 2023
8e8e194
make the demo app show a regular integration path
YazeedAlKhalaf Dec 20, 2023
8d236bb
move the defaultBinaryMessenger for test calling to a variable to inc…
YazeedAlKhalaf Jun 1, 2024
122d7ba
move completion handler after serialization is successful
YazeedAlKhalaf Jun 1, 2024
b09fd8c
enhance tests and their readability
YazeedAlKhalaf Jun 1, 2024
73fa61c
make updatePaymentResult throw an exception instead of failing silently
YazeedAlKhalaf Jun 1, 2024
747ff50
Merge branch 'main' of github.com:google-pay/flutter-plugin into add-…
YazeedAlKhalaf Jun 1, 2024
b39bddd
fix the linter complaining
YazeedAlKhalaf Jun 1, 2024
15fb1aa
just ran the app with flutter 3.22.1
YazeedAlKhalaf Jun 1, 2024
7b64e53
make the implementation better, thanks to @JlUgia
YazeedAlKhalaf Jun 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pay/example/ios/Flutter/AppFrameworkInfo.plist
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>11.0</string>
<string>12.0</string>
</dict>
</plist>
2 changes: 1 addition & 1 deletion pay/example/ios/Podfile
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
8 changes: 4 additions & 4 deletions pay/example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 5 additions & 2 deletions pay/example/ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 51;
objectVersion = 54;
objects = {

/* Begin PBXBuildFile section */
Expand Down Expand Up @@ -170,7 +170,7 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1300;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
Expand Down Expand Up @@ -237,10 +237,12 @@
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
Expand Down Expand Up @@ -268,6 +270,7 @@
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1300"
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
2 changes: 2 additions & 0 deletions pay/example/ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,7 @@
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
YazeedAlKhalaf marked this conversation as resolved.
Show resolved Hide resolved
<true/>
</dict>
</plist>
52 changes: 31 additions & 21 deletions pay/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
);
}
}
Expand All @@ -64,16 +65,23 @@ class _PaySampleAppState extends State<PaySampleApp> {
@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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was under the impression that we decided to go with the alternative callback approach (see comment and let me know)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah okay, not sure why i did it with one method, but basically i need to keep the old method and have this new method also, right?

just want to make sure 😁

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
Expand Down Expand Up @@ -128,23 +136,25 @@ class _PaySampleAppState extends State<PaySampleApp> {
),
// Example pay button configured using an asset
FutureBuilder<PaymentConfiguration>(
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,
Expand Down
30 changes: 29 additions & 1 deletion pay/lib/src/pay.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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].
///
Expand Down Expand Up @@ -70,6 +73,19 @@ class Pay {
_configurations[provider]!, paymentItems);
}

/// Update the payment result with the native platform.
/// Works only on iOS.
Future<void> 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<void> throwIfProviderIsNotDefined(PayProvider provider) async {
Expand All @@ -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';
}
20 changes: 18 additions & 2 deletions pay/lib/src/widgets/apple_pay_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@

part of '../../pay.dart';

class ApplePaymentConfirmation {
final Pay _payClient;
ApplePaymentConfirmation(Pay payClient) : _payClient = payClient;

Future<void> updatePaymentResult(bool completedSuccessfully) async {
await _payClient.updatePaymentResult(completedSuccessfully);
}
}

typedef ApplePaymentConfirmCallback = Function(
Map<String, dynamic> result, ApplePaymentConfirmation handler);

/// A widget to show the Apple Pay button according to the rules and constraints
/// specified in [PayButton].
///
Expand All @@ -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<PaymentItem> paymentItems,
double? cornerRadius,
ApplePayButtonStyle style = ApplePayButtonStyle.black,
Expand All @@ -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,
Expand Down
11 changes: 9 additions & 2 deletions pay/lib/src/widgets/google_pay_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

part of '../../pay.dart';

typedef GooglePaymentCallback = Function(Map<String, dynamic> result);

/// A widget to show the Google Pay button according to the rules and
/// constraints specified in [PayButton].
///
Expand All @@ -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<PaymentItem> paymentItems,
int cornerRadius = RawGooglePayButton.defaultButtonHeight ~/ 2,
GooglePayButtonTheme theme = GooglePayButtonTheme.dark,
Expand All @@ -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,
Expand Down
10 changes: 6 additions & 4 deletions pay/lib/src/widgets/pay_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

part of '../../pay.dart';

typedef PaymentResultCallback = void Function(Map<String, dynamic> result);

/// A widget that handles the API logic to facilitate the integration.
///
/// This widget provides an alternative UI-based integration path that wraps
Expand All @@ -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<String, dynamic> result)? onPaymentResult;
final PaymentResultCallback? paymentCallback;

final double width;
final double height;
Expand All @@ -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),
Expand All @@ -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);
}
Expand Down
5 changes: 5 additions & 0 deletions pay_ios/ios/Classes/PayPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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)
}
Expand Down
Loading