diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..acd575f6 --- /dev/null +++ b/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/android/app/src/main/kotlin/co/paystack/example/MainActivity.kt b/example/android/app/src/main/kotlin/co/paystack/example/MainActivity.kt new file mode 100644 index 00000000..004d6c03 --- /dev/null +++ b/example/android/app/src/main/kotlin/co/paystack/example/MainActivity.kt @@ -0,0 +1,6 @@ +package co.paystack.example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/example/android/app/src/main/res/drawable-v21/launch_background.xml b/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 00000000..f74085f3 --- /dev/null +++ b/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/example/android/app/src/main/res/values-night/styles.xml b/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 00000000..449a9f93 --- /dev/null +++ b/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/example/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 00000000..acd575f6 --- /dev/null +++ b/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift new file mode 100644 index 00000000..70693e4a --- /dev/null +++ b/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/example/ios/Runner/Runner-Bridging-Header.h b/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 00000000..308a2a56 --- /dev/null +++ b/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/example/lib/main.dart b/example/lib/main.dart index 14e5eeb6..cb2ba4a7 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -5,6 +5,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_paystack/flutter_paystack.dart'; import 'package:http/http.dart' as http; +import 'package:flutter/foundation.dart'; // To get started quickly, change this to your heroku deployment of // https://github.com/PaystackHQ/sample-charge-card-backend @@ -15,10 +16,15 @@ import 'package:http/http.dart' as http; // Step 5. Replace {YOUR_BACKEND_URL} below with the url generated by heroku (format https://some-url.herokuapp.com) String backendUrl = '{YOUR_BACKEND_URL}'; // Set this to a public key that matches the secret key you supplied while creating the heroku instance -String paystackPublicKey = '{YOUR_PAYSTACK_PUBLIC_KEY}'; +// String paystackPublicKey = '{YOUR_PAYSTACK_PUBLIC_KEY}'; +String paystackPublicKey = ''; const String appName = 'Paystack Example'; -void main() => runApp(new MyApp()); +void main() { + if (kIsWeb) WidgetsFlutterBinding.ensureInitialized(); + + runApp(new MyApp()); +} class MyApp extends StatelessWidget { @override @@ -53,7 +59,9 @@ class _HomePageState extends State { String? _cvv; int? _expiryMonth; int? _expiryYear; - + String _email = ''; + int _amount = 0; + String _message = ''; @override void initState() { plugin.initialize(publicKey: paystackPublicKey); @@ -62,181 +70,257 @@ class _HomePageState extends State { @override Widget build(BuildContext context) { - return new Scaffold( - key: _scaffoldKey, - appBar: new AppBar(title: const Text(appName)), - body: new Container( - padding: const EdgeInsets.all(20.0), - child: new Form( - key: _formKey, - child: new SingleChildScrollView( - child: new ListBody( - children: [ - new Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - new Expanded( - child: const Text('Initalize transaction from:'), + return kIsWeb + ? new Scaffold( + appBar: AppBar( + title: Text('Flutter Paystack Test'), + ), + body: Padding( + padding: EdgeInsets.symmetric( + horizontal: MediaQuery.of(context).size.width / 8, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + _message, + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.deepPurple, ), - new Expanded( - child: new Column( - mainAxisSize: MainAxisSize.min, - children: [ - new RadioListTile( - value: 0, - groupValue: _radioValue, - onChanged: _handleRadioValueChanged, - title: const Text('Local'), - ), - new RadioListTile( - value: 1, - groupValue: _radioValue, - onChanged: _handleRadioValueChanged, - title: const Text('Server'), - ), - ]), - ) - ], - ), - _border, - _verticalSizeBox, - new TextFormField( - decoration: const InputDecoration( - border: const UnderlineInputBorder(), - labelText: 'Card number', ), - onSaved: (String? value) => _cardNumber = value, - ), - _verticalSizeBox, - new Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - new Expanded( - child: new TextFormField( - decoration: const InputDecoration( - border: const UnderlineInputBorder(), - labelText: 'CVV', - ), - onSaved: (String? value) => _cvv = value, - ), + const SizedBox(height: 10), + TextField( + decoration: InputDecoration( + labelText: 'Email', ), - _horizontalSizeBox, - new Expanded( - child: new TextFormField( - decoration: const InputDecoration( - border: const UnderlineInputBorder(), - labelText: 'Expiry Month', - ), - onSaved: (String? value) => - _expiryMonth = int.tryParse(value ?? ""), - ), + keyboardType: TextInputType.emailAddress, + onChanged: (val) { + _email = val; + }, + ), + TextField( + decoration: InputDecoration( + labelText: 'Amount', ), - _horizontalSizeBox, - new Expanded( - child: new TextFormField( + keyboardType: TextInputType.number, + onChanged: (val) { + try { + _amount = (double.parse(val) * 100).toInt(); + } catch (e) {} + }, + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: () async { + setState(() { + _message = ''; + }); + + final charge = Charge() + ..email = _email + ..amount = _amount + ..reference = + 'ref_${DateTime.now().millisecondsSinceEpoch}'; + final res = + await plugin.checkout(context, charge: charge); + + if (res.status) { + _message = + 'Charge was successful. Ref: ${res.reference}'; + } else { + _message = 'Failed: ${res.message}'; + } + setState(() {}); + }, + child: Text('Checkout'), + ), + ], + ), + ), + ) + : new Scaffold( + key: _scaffoldKey, + appBar: new AppBar(title: const Text(appName)), + body: new Container( + padding: const EdgeInsets.all(20.0), + child: new Form( + key: _formKey, + child: new SingleChildScrollView( + child: new ListBody( + children: [ + new Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + new Expanded( + child: const Text('Initalize transaction from:'), + ), + new Expanded( + child: new Column( + mainAxisSize: MainAxisSize.min, + children: [ + new RadioListTile( + value: 0, + groupValue: _radioValue, + onChanged: _handleRadioValueChanged, + title: const Text('Local'), + ), + new RadioListTile( + value: 1, + groupValue: _radioValue, + onChanged: _handleRadioValueChanged, + title: const Text('Server'), + ), + ]), + ) + ], + ), + _border, + _verticalSizeBox, + new TextFormField( decoration: const InputDecoration( border: const UnderlineInputBorder(), - labelText: 'Expiry Year', + labelText: 'Card number', ), - onSaved: (String? value) => - _expiryYear = int.tryParse(value ?? ""), + onSaved: (String? value) => _cardNumber = value, ), - ) - ], - ), - _verticalSizeBox, - Theme( - data: Theme.of(context).copyWith( - accentColor: green, - primaryColorLight: Colors.white, - primaryColorDark: navyBlue, - textTheme: Theme.of(context).textTheme.copyWith( - bodyText2: TextStyle( - color: lightBlue, + _verticalSizeBox, + new Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + new Expanded( + child: new TextFormField( + decoration: const InputDecoration( + border: const UnderlineInputBorder(), + labelText: 'CVV', + ), + onSaved: (String? value) => _cvv = value, + ), ), - ), - ), - child: Builder( - builder: (context) { - return _inProgress - ? new Container( - alignment: Alignment.center, - height: 50.0, - child: Platform.isIOS - ? new CupertinoActivityIndicator() - : new CircularProgressIndicator(), - ) - : new Column( - mainAxisSize: MainAxisSize.min, - children: [ - _getPlatformButton( - 'Charge Card', () => _startAfreshCharge()), - _verticalSizeBox, - _border, - new SizedBox( - height: 40.0, + _horizontalSizeBox, + new Expanded( + child: new TextFormField( + decoration: const InputDecoration( + border: const UnderlineInputBorder(), + labelText: 'Expiry Month', + ), + onSaved: (String? value) => + _expiryMonth = int.tryParse(value ?? ""), + ), + ), + _horizontalSizeBox, + new Expanded( + child: new TextFormField( + decoration: const InputDecoration( + border: const UnderlineInputBorder(), + labelText: 'Expiry Year', + ), + onSaved: (String? value) => + _expiryYear = int.tryParse(value ?? ""), + ), + ) + ], + ), + _verticalSizeBox, + Theme( + data: Theme.of(context).copyWith( + accentColor: green, + primaryColorLight: Colors.white, + primaryColorDark: navyBlue, + textTheme: Theme.of(context).textTheme.copyWith( + bodyText2: TextStyle( + color: lightBlue, ), - new Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - new Flexible( - flex: 3, - child: new DropdownButtonHideUnderline( - child: new InputDecorator( - decoration: const InputDecoration( - border: OutlineInputBorder(), - isDense: true, - hintText: 'Checkout method', + ), + ), + child: Builder( + builder: (context) { + return _inProgress + ? new Container( + alignment: Alignment.center, + height: 50.0, + child: Platform.isIOS + ? new CupertinoActivityIndicator() + : new CircularProgressIndicator(), + ) + : new Column( + mainAxisSize: MainAxisSize.min, + children: [ + _getPlatformButton('Charge Card', + () => _startAfreshCharge()), + _verticalSizeBox, + _border, + new SizedBox( + height: 40.0, + ), + new Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: + CrossAxisAlignment.center, + children: [ + new Flexible( + flex: 3, + child: + new DropdownButtonHideUnderline( + child: new InputDecorator( + decoration: + const InputDecoration( + border: OutlineInputBorder(), + isDense: true, + hintText: 'Checkout method', + ), + child: new DropdownButton< + CheckoutMethod>( + value: _method, + isDense: true, + onChanged: + (CheckoutMethod? value) { + if (value != null) { + setState(() => + _method = value); + } + }, + items: + banks.map((String value) { + return new DropdownMenuItem< + CheckoutMethod>( + value: + _parseStringToMethod( + value), + child: new Text(value), + ); + }).toList(), + ), + ), + ), ), - child: new DropdownButton< - CheckoutMethod>( - value: _method, - isDense: true, - onChanged: (CheckoutMethod? value) { - if (value != null) { - setState(() => _method = value); - } - }, - items: banks.map((String value) { - return new DropdownMenuItem< - CheckoutMethod>( - value: - _parseStringToMethod(value), - child: new Text(value), - ); - }).toList(), + _horizontalSizeBox, + new Flexible( + flex: 2, + child: new Container( + width: double.infinity, + child: _getPlatformButton( + 'Checkout', + () => _handleCheckout(context), + ), + ), ), - ), - ), - ), - _horizontalSizeBox, - new Flexible( - flex: 2, - child: new Container( - width: double.infinity, - child: _getPlatformButton( - 'Checkout', - () => _handleCheckout(context), - ), - ), - ), - ], - ) - ], - ); - }, + ], + ) + ], + ); + }, + ), + ) + ], ), - ) - ], + ), + ), ), - ), - ), - ), - ); + ); } void _handleRadioValueChanged(int? value) { diff --git a/example/web/favicon.png b/example/web/favicon.png new file mode 100644 index 00000000..8aaa46ac Binary files /dev/null and b/example/web/favicon.png differ diff --git a/example/web/icons/Icon-192.png b/example/web/icons/Icon-192.png new file mode 100644 index 00000000..b749bfef Binary files /dev/null and b/example/web/icons/Icon-192.png differ diff --git a/example/web/icons/Icon-512.png b/example/web/icons/Icon-512.png new file mode 100644 index 00000000..88cfd48d Binary files /dev/null and b/example/web/icons/Icon-512.png differ diff --git a/example/web/index.html b/example/web/index.html new file mode 100644 index 00000000..66f59294 --- /dev/null +++ b/example/web/index.html @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + example + + + + + + + + + diff --git a/example/web/manifest.json b/example/web/manifest.json new file mode 100644 index 00000000..8c012917 --- /dev/null +++ b/example/web/manifest.json @@ -0,0 +1,23 @@ +{ + "name": "example", + "short_name": "example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/lib/src/common/paystack.dart b/lib/src/common/paystack.dart index fcf2df33..e150d77b 100644 --- a/lib/src/common/paystack.dart +++ b/lib/src/common/paystack.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -13,12 +14,14 @@ import 'package:flutter_paystack/src/models/card.dart'; import 'package:flutter_paystack/src/models/charge.dart'; import 'package:flutter_paystack/src/models/checkout_response.dart'; import 'package:flutter_paystack/src/transaction/card_transaction_manager.dart'; +import 'package:flutter_paystack/src/web/paystack_web.dart'; import 'package:flutter_paystack/src/widgets/checkout/checkout_widget.dart'; class PaystackPlugin { bool _sdkInitialized = false; String _publicKey = ""; static late PlatformInfo platformInfo; + static var _web = PaystackWeb(); /// Initialize the Paystack object. It should be called as early as possible /// (preferably in initState() of the Widget. @@ -42,7 +45,6 @@ class PaystackPlugin { // Using cascade notation to build the platform specific info try { - platformInfo = await PlatformInfo.fromMethodChannel(Utils.methodChannel); _sdkInitialized = true; } on PlatformException { @@ -128,6 +130,8 @@ class PaystackPlugin { bool hideEmail = false, bool hideAmount = false, }) async { + if (kIsWeb) return _web.checkout(charge, _publicKey); + return _Paystack(publicKey).checkout( context, charge: charge, diff --git a/lib/src/common/platform_info.dart b/lib/src/common/platform_info.dart index f260da2a..c234df22 100644 --- a/lib/src/common/platform_info.dart +++ b/lib/src/common/platform_info.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; /// Holds data that's different on Android and iOS @@ -13,9 +14,19 @@ class PlatformInfo { // And there should a better way to fucking do this final pluginVersion = "1.0.5"; - final platform = Platform.operatingSystem; + /// Fixing This + // final platform = Platform.operatingSystem; + String platform() { + if (kIsWeb) { + return "Web"; + } else { + return Platform.operatingSystem; + } + } + String userAgent = "${platform}_Paystack_$pluginVersion"; - String deviceId = await channel.invokeMethod('getDeviceId') ?? ""; + String deviceId = + kIsWeb ? platform() : await channel.invokeMethod('getDeviceId') ?? ""; return PlatformInfo._( userAgent: userAgent, paystackBuild: pluginVersion, diff --git a/lib/src/web/js/js_stub.dart b/lib/src/web/js/js_stub.dart new file mode 100644 index 00000000..1923146d --- /dev/null +++ b/lib/src/web/js/js_stub.dart @@ -0,0 +1 @@ +external F allowInterop(F f); diff --git a/lib/src/web/js/paystack_js.dart b/lib/src/web/js/paystack_js.dart new file mode 100644 index 00000000..9b39878d --- /dev/null +++ b/lib/src/web/js/paystack_js.dart @@ -0,0 +1,55 @@ +@JS() +library paystack_js; + +import 'package:flutter/material.dart'; +import "package:js/js.dart"; + +@JS('PaystackPop') +class PaystackPop { + @JS('setup') + external static Handler setup(SetupData data); +} + +@JS() +@anonymous +class Handler { + external void openIframe(); +} + +@JS() +@anonymous +class SetupData { + external factory SetupData({ + required String key, + required String email, + required int amount, + required String ref, + VoidCallback? onClose, + PaystackCallback? callback, + }); + + external String key; + external String email; + external int amount; + external String ref; + external VoidCallback onClose; + external PaystackCallback callback; +} + +@JS() +@anonymous +class ChargeResponse { + external String get message; + + external String get reference; + + external String get response; + + external String get status; +} + +typedef PaystackCallback(ChargeResponse response); + +Handler setup(SetupData data) { + return PaystackPop.setup(data); +} diff --git a/lib/src/web/js/paystack_stub.dart b/lib/src/web/js/paystack_stub.dart new file mode 100644 index 00000000..4297a03d --- /dev/null +++ b/lib/src/web/js/paystack_stub.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; + +Handler setup(SetupData data) { + throw UnimplementedError(); +} + +abstract class Handler { + void openIframe(); +} + +class SetupData { + final String? key; + final String? email; + final int amount; + final String? ref; + + final VoidCallback? onClose; + final PaystackCallback? callback; + + SetupData({ + required this.key, + required this.email, + required this.amount, + required this.ref, + this.onClose, + this.callback, + }); +} + +abstract class ChargeResponse { + String get message; + + String get reference; + + String get response; + + String get status; +} + +typedef PaystackCallback(ChargeResponse response); diff --git a/lib/src/web/paystack_web.dart b/lib/src/web/paystack_web.dart new file mode 100644 index 00000000..529b30d1 --- /dev/null +++ b/lib/src/web/paystack_web.dart @@ -0,0 +1,42 @@ +import 'dart:async'; + +import 'package:flutter_paystack/flutter_paystack.dart'; + +import 'js/js_stub.dart' if (dart.library.js) 'package:js/js.dart'; +import 'js/paystack_stub.dart' if (dart.library.js) 'js/paystack_js.dart'; + +class PaystackWeb { + Future checkout(Charge charge, String key) async { + final completer = Completer(); + + final handler = setup( + SetupData( + key: key, + email: charge.email!, + amount: charge.amount, + ref: charge.reference!, + onClose: allowInterop( + () { + completer.complete(CheckoutResponse.defaults()); + }, + ), + callback: allowInterop((response) { + completer.complete( + CheckoutResponse( + message: response.message, + reference: response.reference, + status: response.status == 'success', + method: CheckoutMethod.card, + verify: true, + card: charge.card ?? PaymentCard.empty(), + ), + ); + }), + ), + ); + + handler.openIframe(); + + return completer.future; + } +} diff --git a/pubspec.yaml b/pubspec.yaml index eb3a9b5a..8d56f5a2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,6 +11,7 @@ dependencies: intl: ^0.17.0 meta: ^1.3.0 async: ^2.5.0 + js: ^0.6.3 dev_dependencies: flutter_test: