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

Update Flutter v3.27.1 #1

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@

REEPAY_CHECKOUT_API_SESSION_CHARGE=https://checkout-api.reepay.com/v1/session/charge
REEPAY_API_CUSTOMER=https://api.reepay.com/v1/customer
REEPAY_PRIVATE_API_KEY=<insert your api key here>
REEPAY_PRIVATE_API_KEY=<insert your api key here>

# URL to your test checkout session directly
TEST_CHECKOUT_SESSION_URL=""
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/

# IntelliJ related
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,14 @@ flutter pub get
- [Events](#events)
- [Url path changes](#url-path-changes)
- [Extra](#extra)
- [Handling events](#handling-events-newly-added-in-v110)
- [Usage](#usage)
- [Reepay Private API Key](#reepay-private-api-key)
- [Custom URL schemes](#custom-url-schemes)

## Available Scripts

This project is built with `Flutter version 3.19.0`. Before running your Flutter app, you must create iOS and Android platforms respectively.
This project is built with `Flutter version 3.27.1 and Dart version 3.6.0`. Before running your Flutter app, you must create iOS and Android platforms respectively.

### flutter create ios platform

Expand Down Expand Up @@ -68,7 +69,7 @@ flutter run -d <device_id>
```

## Events
In the app, we will use URL path changes as events that WebView listens to, thus checking whether URL contains `accept` or `cancel` in the path.
In the app, we will use URL path changes as events that WebView listens to, thus checking whether URL contains `accept` or `cancel` in the path. In version 1.1.0, we have introduced new event handling via postMessage from the WebView. Read more about handling events below.

### URL path changes
As we are using WebView by passing session URL, we will receive response with as either Accept URL or Cancel URL as defined in the request body [docs](https://docs.reepay.com/reference/createchargesession):
Expand All @@ -84,6 +85,9 @@ In the WebView, we will listen to URL changes when the checkout has completed a
### Extra
For additional parameters to be passed, use query parameters in `accept_url` or `cancel_url`. For example, `https://webshop.com/decline/order-12345?myEvent=someValue&yourEvent=anotherValue`.

### Handling events `Newly added in v1.1.0`
The app will now receive events via postMessage from the WebView. The WebView will send a message with the event type and the event data. The app will then handle the event accordingly. See `checkout_screen.dart` for more details.

## Usage

1. Run your Flutter app with `flutter run`.
Expand Down
21 changes: 21 additions & 0 deletions lib/checkout/domain/models/checkout_state_enum.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Based on: https://optimize-docs.billwerk.com/docs/checkoutstate-enum

enum ECheckoutState {
init('Init'),
open('Open'),
accept('Accept'),
cancel('Cancel'),
close('Close'),
error('Error');

final String value;

const ECheckoutState(this.value);

static ECheckoutState fromString(String value) {
return ECheckoutState.values.firstWhere(
(state) => state.value == value,
orElse: () => throw ArgumentError('Invalid value: $value'),
);
}
}
16 changes: 16 additions & 0 deletions lib/checkout/domain/models/user_action_enum.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Based on: https://optimize-docs.billwerk.com/docs/useraction-enum

enum EUserAction {
cardInputChange('card_input_change');

final String value;

const EUserAction(this.value);

static EUserAction fromString(String value) {
return EUserAction.values.firstWhere(
(state) => state.value == value,
orElse: () => throw ArgumentError('Invalid value: $value'),
);
}
}
2 changes: 1 addition & 1 deletion lib/checkout/domain/services/checkout_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ class CheckoutService implements CheckoutRepository {
Uri.parse("https://api.reepay.com/v1/customer/$customerHandle"),
);

String encoded = base64.encode(utf8.encode("priv_b3aae30490f8f7792fc0ce659b2380f4"));
String encoded = base64.encode(utf8.encode(dotenv.env['REEPAY_PRIVATE_API_KEY'] as String));
request.headers.set("content-type", "application/json");
request.headers.set("accept", "application/json");
request.headers.set("Authorization", encoded);
Expand Down
132 changes: 121 additions & 11 deletions lib/checkout/screens/checkout_screen.dart
Original file line number Diff line number Diff line change
@@ -1,28 +1,40 @@
// ignore_for_file: prefer_const_constructors
import 'dart:async';
import 'dart:convert';

import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:reepay_checkout_flutter_example/checkout/domain/models/checkout_state_enum.dart';
import 'package:reepay_checkout_flutter_example/checkout/domain/models/user_action_enum.dart';
import 'package:reepay_checkout_flutter_example/checkout/index.dart';
import 'package:reepay_checkout_flutter_example/utils/event_parser.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:webview_flutter_android/webview_flutter_android.dart';
import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';

class CheckoutScreen extends StatefulWidget {
const CheckoutScreen({super.key});
final Future<Map<String, dynamic>>? sessionData;

const CheckoutScreen({super.key, this.sessionData});

@override
State<CheckoutScreen> createState() => _CheckoutScreenState();
}

class _CheckoutScreenState extends State<CheckoutScreen> {
late Future<Map> sessionData;
late WebViewController _controller;
late Future<Map<String, dynamic>> sessionData;

@override
void initState() {
super.initState();
sessionData = CheckoutService().getSessionUrl(CheckoutProvider().customerHandle, CheckoutProvider().orderlines());

sessionData = widget.sessionData ??
CheckoutService().getSessionUrl(
CheckoutProvider().customerHandle,
CheckoutProvider().orderlines(),
);
}

@override
Expand Down Expand Up @@ -85,14 +97,19 @@ class _CheckoutScreenState extends State<CheckoutScreen> {
onCancel();
return NavigationDecision.prevent;
} else if (request.url.contains("accept")) {
onAccept();
onAccept(hasCustomerInfo: true);
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
),
)
..addJavaScriptChannel('CheckoutChannel', onMessageReceived: (JavaScriptMessage message) {
_onMessageReceived(message);
})
..loadRequest(Uri.parse(sessionUrl));

_controller = controller;
return controller;
}

Expand Down Expand Up @@ -124,21 +141,114 @@ class _CheckoutScreenState extends State<CheckoutScreen> {
}

/// accept handler
void onAccept() {
void onAccept({hasCustomerInfo = false}) {
print("Payment success");
CheckoutService()
.updateCustomer(
customerHandle: CheckoutProvider().customerHandle,
customer: CheckoutProvider().customer,
)
.then((value) => print('Status - update customer: $value'));
if (hasCustomerInfo) {
CheckoutService()
.updateCustomer(
customerHandle: CheckoutProvider().customerHandle,
customer: CheckoutProvider().customer,
)
.then((value) => print('Status - update customer: $value'));
}
CheckoutProvider().setCart([]);
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => CompletedScreen()),
);
}

/// Handle incoming messages from WebView
void _onMessageReceived(JavaScriptMessage message) {
print("[_onMessageReceived]: ${message.message}");

Map<String, dynamic>? data;
try {
data = json.decode(message.message);
final eventName = data!['event'];
if (eventName is String) {
final event = EventParser.parseEvent(eventName);
if (event is EUserAction) {
_handleUserActions(data);
} else if (event is ECheckoutState) {
_handleEvents(data);
} else {
throw ArgumentError('Invalid event type: $data');
}
} else {
print('Undefined event: $data');
}
} catch (e) {
print('[_onMessageReceived] Error: $e');
}
}

void _handleEvents(data) {
ECheckoutState event = ECheckoutState.fromString(data['event']);
print('Event: $event');

switch (event) {
case ECheckoutState.init:
_controller.runJavaScriptReturningResult('navigator.userAgent').then((result) {
final userAgent = result.toString();
final customUserAgent = '$userAgent ReepayCheckoutDemoApp/1.0.0 (Flutter)';
final reply = {'userAgent': customUserAgent, 'isWebView': true};
final jsCode = '''
if (window.CheckoutChannel && typeof window.CheckoutChannel.resolveMessage === 'function') {
window.CheckoutChannel.resolveMessage(${jsonEncode(reply)});
}
''';
_controller.runJavaScript(jsCode);
}).catchError((error) {
print('Error retrieving user agent: $error');
});
break;
case ECheckoutState.open:
case ECheckoutState.close:
break;
case ECheckoutState.cancel:
onCancel();
break;
case ECheckoutState.accept:
onAccept();
break;
case ECheckoutState.error:
print('Error: $data');
break;
default:
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Unknown event: $event')),
);
return;
}

ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Event: $data')),
);
}

void _handleUserActions(data) {
EUserAction action = EUserAction.fromString(data['event']);
print('User Action: $action');

switch (action) {
case EUserAction.cardInputChange:
final reply = {'isWebViewChanged': true};
final jsCode = '''
if (window.CheckoutChannel && typeof window.CheckoutChannel.resolveMessage === 'function') {
window.CheckoutChannel.resolveMessage(${jsonEncode(reply)});
}
''';
_controller.runJavaScript(jsCode);
break;
default:
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Unknown action: $action')),
);
return;
}
}

///
/// Return HTML template with Reepay Window Checkout element.
/// (Alternatively, use session url directly in webview)
Expand Down
15 changes: 15 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,21 @@ class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateM
},
child: Text("Checkout Docs"),
),
TextButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CheckoutScreen(
sessionData: Future.value({
"url": dotenv.env['TEST_CHECKOUT_SESSION_URL'] as String,
}),
),
),
);
},
child: Text("Test WebView"),
),
Spacer(),
TextButton(
onPressed: () {
Expand Down
20 changes: 20 additions & 0 deletions lib/utils/event_parser.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import 'package:reepay_checkout_flutter_example/checkout/domain/models/checkout_state_enum.dart';
import 'package:reepay_checkout_flutter_example/checkout/domain/models/user_action_enum.dart';

class EventParser {
static Enum? parseEvent(String event) {
for (var state in ECheckoutState.values) {
if (state.value == event) {
return state;
}
}

for (var action in EUserAction.values) {
if (action.value == event) {
return action;
}
}

return null;
}
}
Loading