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 an event stream to communicate with Android's native end #282

Merged
merged 12 commits into from
Oct 10, 2024
48 changes: 36 additions & 12 deletions pay/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ dependencies:
Define the configuration for your payment provider(s). Take a look at the parameters available in the documentation for [Apple Pay](https://developer.apple.com/documentation/passkit/pkpaymentrequest) and [Google Pay](https://developers.google.com/pay/api/android/reference/request-objects), and explore the [sample configurations in this package](https://github.com/google-pay/flutter-plugin/tree/main/pay/example/lib/payment_configurations.dart).

### Example
This snippet assumes the existence a payment configuration for Apple Pay ([`defaultApplePayConfigString`](https://github.com/google-pay/flutter-plugin/tree/main/pay/example/lib/payment_configurations.dart#L27)) and another one for Google Pay ([`defaultGooglePayConfigString`](https://github.com/google-pay/flutter-plugin/tree/main/pay/example/lib/payment_configurations.dart#L63)):
This snippet assumes the existence of a payment configuration for Apple Pay ([`defaultApplePayConfigString`](https://github.com/google-pay/flutter-plugin/tree/main/pay/example/lib/payment_configurations.dart#L27)) and another one for Google Pay ([`defaultGooglePayConfigString`](https://github.com/google-pay/flutter-plugin/tree/main/pay/example/lib/payment_configurations.dart#L63)):
```dart
import 'package:pay/pay.dart';

Expand Down Expand Up @@ -117,15 +117,10 @@ const _paymentItems = [
)
];

late final Pay _payClient;

// When you are ready to load your configuration
_payClient = Pay({
PayProvider.google_pay: PaymentConfiguration.fromJsonString(
payment_configurations.defaultGooglePay),
PayProvider.apple_pay: PaymentConfiguration.fromJsonString(
payment_configurations.defaultApplePay),
});
final Pay _payClient = Pay({
PayProvider.google_pay: payment_configurations.defaultGooglePayConfig,
PayProvider.apple_pay: payment_configurations.defaultApplePayConfig,
});
```

As you can see, you can add multiple configurations to your payment client, one for each payment provider supported.
Expand All @@ -141,8 +136,10 @@ Widget build(BuildContext context) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == true) {
return RawGooglePayButton(
paymentConfiguration:
payment_configurations.defaultGooglePayConfig,
type: GooglePayButtonType.buy,
onPressed: onGooglePayPressed);
onPressed: _onGooglePayPressed);
} else {
// userCanPay returned false
// Consider showing an alternative payment method
Expand All @@ -158,7 +155,7 @@ Widget build(BuildContext context) {
Finally, handle the `onPressed` event and trigger the payment selector as follows:

```dart
void onGooglePayPressed() async {
void _onGooglePayPressed() async {
final result = await _payClient.showPaymentSelector(
PayProvider.google_pay,
_paymentItems,
Expand All @@ -167,6 +164,33 @@ void onGooglePayPressed() async {
}
```

### Handling a payment result response (Android only)
On Android, payment results are received using an event channel, in order to eliminate the effect of lost references during activity recreation events. Because of that, calls to `Pay.showPaymentSelector` only initiate the payment process and don't return any result.

To subscribe to the result stream, create an `EventChannel` using the payment results channel name (`plugins.flutter.io/pay/payment_result`) and start listening to events:

```dart
static const eventChannel =
EventChannel('plugins.flutter.io/pay/payment_result');
final _paymentResultSubscription = eventChannel
.receiveBroadcastStream()
.map((result) => jsonDecode(result as String) as Map<String, dynamic>)
.listen((result) {
// TODO: Send the resulting Google Pay token to your server / PSP
}, onError: (error) {
// TODO: Handle errors
});
```

Make sure to cancel the subscription and clear the reference when it is not needed anymore:

```dart
_paymentResultSubscription.cancel();
_paymentResultSubscription = null;
```

See the [advanced example](https://github.com/google-pay/flutter-plugin/blob/main/pay/example/lib/advanced.dart) to see a working integration.

## Additional resources
Take a look at the following resources to manage your payment accounts and learn more about the APIs for the supported providers:

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>
<true/>
</dict>
</plist>
223 changes: 223 additions & 0 deletions pay/example/lib/advanced.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
// Copyright 2024 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.

import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:pay/pay.dart';

import 'payment_configurations.dart' as payment_configurations;

void main() {
runApp(const PayAdvancedMaterialApp());
}

const googlePayEventChannelName = 'plugins.flutter.io/pay/payment_result';
const _paymentItems = [
PaymentItem(
label: 'Total',
amount: '99.99',
status: PaymentItemStatus.final_price,
)
];

class PayAdvancedMaterialApp extends StatelessWidget {
const PayAdvancedMaterialApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Pay for Flutter Advanced Integration Demo',
localizationsDelegates: const [
...GlobalMaterialLocalizations.delegates,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: const [
Locale('en', ''),
Locale('es', ''),
Locale('de', ''),
],
home: PayAdvancedSampleApp(),
);
}
}

class PayAdvancedSampleApp extends StatefulWidget {
final Pay payClient;

PayAdvancedSampleApp({super.key})
: payClient = Pay({
PayProvider.google_pay: payment_configurations.defaultGooglePayConfig,
PayProvider.apple_pay: payment_configurations.defaultApplePayConfig,
});

@override
State<PayAdvancedSampleApp> createState() => _PayAdvancedSampleAppState();
}

class _PayAdvancedSampleAppState extends State<PayAdvancedSampleApp> {
static const eventChannel = EventChannel(googlePayEventChannelName);
StreamSubscription<String>? _googlePayResultSubscription;

late final Future<bool> _canPayGoogleFuture;
late final Future<bool> _canPayAppleFuture;

// A method to listen to events coming from the event channel on Android
void _startListeningForPaymentResults() {
_googlePayResultSubscription = eventChannel
.receiveBroadcastStream()
.map((result) => result.toString())
.listen(debugPrint, onError: (error) => debugPrint(error.toString()));
}

@override
void initState() {
super.initState();
if (defaultTargetPlatform == TargetPlatform.android) {
_startListeningForPaymentResults();
}

// Initialize userCanPay futures
_canPayGoogleFuture = widget.payClient.userCanPay(PayProvider.google_pay);
_canPayAppleFuture = widget.payClient.userCanPay(PayProvider.apple_pay);
}

void _onGooglePayPressed() =>
_showPaymentSelectorForProvider(PayProvider.google_pay);

void _onApplePayPressed() =>
_showPaymentSelectorForProvider(PayProvider.apple_pay);

void _showPaymentSelectorForProvider(PayProvider provider) async {
try {
final result =
await widget.payClient.showPaymentSelector(provider, _paymentItems);
debugPrint(result.toString());
} catch (error) {
debugPrint(error.toString());
}
}

@override
void dispose() {
_googlePayResultSubscription?.cancel();
_googlePayResultSubscription = null;
super.dispose();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('T-shirt Shop'),
),
backgroundColor: Colors.white,
body: ListView(
padding: const EdgeInsets.symmetric(horizontal: 20),
children: [
Container(
margin: const EdgeInsets.symmetric(vertical: 20),
child: const Image(
image: AssetImage('assets/images/ts_10_11019a.jpg'),
height: 350,
),
),
const Text(
'Amanda\'s Polo Shirt',
style: TextStyle(
fontSize: 20,
color: Color(0xff333333),
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 5),
const Text(
'\$50.20',
style: TextStyle(
color: Color(0xff777777),
fontSize: 15,
),
),
const SizedBox(height: 15),
const Text(
'Description',
style: TextStyle(
fontSize: 15,
color: Color(0xff333333),
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 5),
const Text(
'A versatile full-zip that you can wear all day long and even...',
style: TextStyle(
color: Color(0xff777777),
fontSize: 15,
),
),
const SizedBox(height: 15),

// Google Pay button
FutureBuilder<bool>(
future: _canPayGoogleFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == true) {
return RawGooglePayButton(
paymentConfiguration:
payment_configurations.defaultGooglePayConfig,
type: GooglePayButtonType.buy,
onPressed: _onGooglePayPressed);
} else {
// userCanPay returned false
// Consider showing an alternative payment method
}
} else {
// The operation hasn't finished loading
// Consider showing a loading indicator
}
// This example shows an empty box if userCanPay returns false
return const SizedBox.shrink();
},
),

// Apple Pay button
FutureBuilder<bool>(
future: _canPayAppleFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == true) {
return RawApplePayButton(
type: ApplePayButtonType.buy,
onPressed: _onApplePayPressed);
} else {
// userCanPay returned false
// Consider showing an alternative payment method
}
} else {
// The operation hasn't finished loading
// Consider showing a loading indicator
}
// This example shows an empty box if userCanPay returns false
return const SizedBox.shrink();
},
),
],
),
);
}
}
10 changes: 10 additions & 0 deletions pay/example/lib/payment_configurations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@
/// application.
library;

import 'package:pay/pay.dart';

/// Sample [PaymentConfiguration] for Apple Pay
final defaultApplePayConfig =
PaymentConfiguration.fromJsonString(defaultApplePay);

/// Sample configuration for Apple Pay. Contains the same content as the file
/// under `assets/default_payment_profile_apple_pay.json`.
const String defaultApplePay = '''{
Expand Down Expand Up @@ -59,6 +65,10 @@ const String defaultApplePay = '''{
}
}''';

/// Sample [PaymentConfiguration] for Google Pay
final defaultGooglePayConfig =
PaymentConfiguration.fromJsonString(defaultGooglePay);

/// Sample configuration for Google Pay. Contains the same content as the file
/// under `assets/default_payment_profile_google_pay.json`.
const String defaultGooglePay = '''{
Expand Down
2 changes: 2 additions & 0 deletions pay/lib/pay.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@

library pay;

import 'dart:convert';
import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pay_ios/pay_ios.dart';
import 'package:pay_android/pay_android.dart';
import 'package:pay_platform_interface/core/payment_configuration.dart';
Expand Down
3 changes: 3 additions & 0 deletions pay/lib/src/widgets/apple_pay_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,7 @@ class ApplePayButton extends PayButton {

@override
late final Widget _payButton = _applePayButton;

@override
final bool _returnsPaymentDataSynchronously = true;
JlUgia marked this conversation as resolved.
Show resolved Hide resolved
}
Loading