diff --git a/README.md b/README.md index 2f2c478..3053af1 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,64 @@ # paystack_flutter -A new Flutter plugin project. +A [Paystack](https://paystack.com) plugin for accepting payments in your Flutter application. + +## Minimum Requirement +Paystack Flutter SDK builds upon the recent patterns in the Android and iOS, thus your app should target: +- Android 6.0 (API level 23) and above +- iOS 13 and above + +> [! Android Gotcha] +> Flutter (3.3.0 below, at the moment) extends the `FlutterActivity` as the base class for Android. The `FlutterActivity` doesn't have the `ComponentActivity`, a compulsory necessity for loading the payment views with the SDK, in its ancestral tree. To fix this, change the `FlutterActivity` to `FlutterFragmentActivity` in the `MainActivity` in the `android` folder of your project. ## Getting Started +- Install the dependency in your project +```sh +flutter pub get link_coming_soon +``` +- Import the paystack_flutter into your `.dart` file +```dart +import 'package:paystack_flutter/paystack.dart'; +``` +- Use the plugin +```dart +final _publicKey = "pk_domain_xxxxxx"; +final _accessCode = "67joTry7t1jz2o"; +final _paystack = Paystack(); + + initialize(String publicKey) async { + try { + final response = await _paystack.initialize(publicKey, true); + if (response) { + log("Sucessfully initialised the SDK"); + } else { + log("Unable to initialise the SDK"); + } + } on PlatformException catch (e) { + log(e.message!); + } + } + + launch() async { + String reference = ""; + try { + final response = await _paystack.launch(_accessCode); + if (response.status) { + reference = response.reference; + log(reference); + } else { + log(response.message); + } + } on PlatformException catch (e) { + log(e.message!); + } + + setState(() { + _reference = reference; + }); + } +``` -This project is a starting point for a Flutter -[plug-in package](https://flutter.dev/developing-packages/), -a specialized package that includes platform-specific implementation code for -Android and/or iOS. +## License +WIP -For help getting started with Flutter development, view the -[online documentation](https://flutter.dev/docs), which offers tutorials, -samples, guidance on mobile development, and a full API reference. diff --git a/android/src/main/kotlin/com/paystack/paystack_flutter/PaystackFlutterPlugin.kt b/android/src/main/kotlin/com/paystack/paystack_flutter/PaystackFlutterPlugin.kt index 9206c4c..f754aeb 100644 --- a/android/src/main/kotlin/com/paystack/paystack_flutter/PaystackFlutterPlugin.kt +++ b/android/src/main/kotlin/com/paystack/paystack_flutter/PaystackFlutterPlugin.kt @@ -1,6 +1,5 @@ package com.paystack.paystack_flutter -import android.util.Log import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import com.paystack.android.core.Paystack @@ -20,54 +19,94 @@ class PaystackFlutterPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { private lateinit var channel : MethodChannel private lateinit var paymentSheet: PaymentSheet private var activity: FlutterFragmentActivity? = null + private lateinit var pendingResult: Result - private fun initializePaystack(publicKey: String, enableLogging: Boolean) { - Paystack - .builder() - .setPublicKey(publicKey) - .setLoggingEnabled(enableLogging) - .build() + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.paystack.flutter") + channel.setMethodCallHandler(this) } - private fun launch(accessCode: String) { - paymentSheet.launch(accessCode) + // TODO: Change error codes to numbers and provide documentation for it + override fun onMethodCall(call: MethodCall, result: Result) { + pendingResult = result + when (call.method) { + "initialize" -> { + val publicKey = call.argument("publicKey") + val enableLogging = call.argument("enableLogging") ?: false + if (publicKey.isNullOrEmpty()) { + result.error("INVALID_ARGUMENTS", "Missing public key", null) + return + } + initialize(publicKey, enableLogging) + } + "launch" -> { + val accessCode = call.argument("accessCode") + if(accessCode.isNullOrEmpty()) { + result.error("INVALID_ARGUMENTS", "Missing access code", null) + return + } + launch(accessCode) + } + else -> result.notImplemented() + } } - // TODO: Change return from String to PaymentSheetResult - // This will require create the same response model in Flutter - private fun paymentComplete(paymentSheetResult: PaymentSheetResult): String { - val response = when (paymentSheetResult) { - is PaymentSheetResult.Cancelled -> "Cancelled" - is PaymentSheetResult.Failed -> paymentSheetResult.error.message ?: "Failed" - is PaymentSheetResult.Completed -> { - paymentSheetResult.paymentCompletionDetails.reference - } + private fun initialize(publicKey: String, enableLogging: Boolean) { + try { + Paystack + .builder() + .setPublicKey(publicKey) + .setLoggingEnabled(enableLogging) + .build() + pendingResult.success(true) + } catch (e: Exception) { + pendingResult.error("INITIALIZATION_ERROR", e.message, null) } - return response +// pendingResult = null } - override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - channel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.paystack.flutter") - channel.setMethodCallHandler(this) + private fun launch(accessCode: String) { + if (activity == null) { + pendingResult.error("NO_ACTIVITY", "Activity is not available", null) + return + } + + try { + paymentSheet.launch(accessCode) + } catch (e: Exception) { + pendingResult.error("LAUNCH_ERROR", e.message, null) + return + } + +// pendingResult = null } - // TODO: Return object in result as opposed to String - override fun onMethodCall(call: MethodCall, result: Result) { - when (call.method) { - "getPlatformVersion" -> result.success("Android ${android.os.Build.VERSION.RELEASE}") - "build" -> { - initializePaystack( - call.argument("publicKey") ?: "", - call.argument("enableLogging") ?: false) - result.success("Initiated") + private fun paymentComplete(paymentSheetResult: PaymentSheetResult) { + when (paymentSheetResult) { + is PaymentSheetResult.Completed -> { + pendingResult.success(mapOf( + "status" to true, + "message" to "Transaction successful", + "reference" to paymentSheetResult.paymentCompletionDetails.reference + )) } - "launch" -> { - launch(call.argument("accessCode") ?: "") - result.success("Launch called") + is PaymentSheetResult.Cancelled -> { + pendingResult.success(mapOf( + "status" to false, + "message" to "Transaction cancelled", + "reference" to "" + )) + } + is PaymentSheetResult.Failed -> { + pendingResult.success(mapOf( + "status" to "failed", + "message" to paymentSheetResult.error.message, + "reference" to "" + )) } - else -> result.notImplemented() } +// pendingResult = null } override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { @@ -76,12 +115,10 @@ class PaystackFlutterPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { override fun onAttachedToActivity(binding: ActivityPluginBinding) { activity = binding.activity as FlutterFragmentActivity - Log.d("ACTIVITY: ", "Attached") (binding.lifecycle as HiddenLifecycleReference) .lifecycle .addObserver(LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_CREATE) { - Log.d("OBSERVER: ", "In observer") val activity = requireNotNull(activity) paymentSheet = PaymentSheet(activity, ::paymentComplete) } diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 7c56964..bb702c6 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -2,25 +2,25 @@ - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - MinimumOSVersion - 12.0 + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 14.0 diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 9c4c1b8..a16608d 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,21 +1,15 @@ PODS: - Flutter (1.0.0) - - paystack_flutter (0.0.1): - - Flutter DEPENDENCIES: - Flutter (from `Flutter`) - - paystack_flutter (from `.symlinks/plugins/paystack_flutter/ios`) EXTERNAL SOURCES: Flutter: :path: Flutter - paystack_flutter: - :path: ".symlinks/plugins/paystack_flutter/ios" SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - paystack_flutter: 2473137a59e6883a1d76de6a4c5116bb1a776270 PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index ae0fadc..4cde320 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -13,10 +13,10 @@ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 59DAD6B6F45CD9120FCC8372 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD19D02F663B345C90C68307 /* Pods_Runner.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -163,7 +163,6 @@ F4AA79E6637F354B101A2511 /* Pods-RunnerTests.release.xcconfig */, 97B1551261DB81BCF2BBDA28 /* Pods-RunnerTests.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -190,9 +189,6 @@ productType = "com.apple.product-type.bundle.unit-test"; }; 97C146ED1CF9000F007C117D /* Runner */ = { - packageProductDependencies = ( - 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, - ); isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( @@ -203,13 +199,15 @@ 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 51CAF0D65C0347913F051B6B /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( ); name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; @@ -218,9 +216,6 @@ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { - packageReferences = ( - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, - ); isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; @@ -246,6 +241,9 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; @@ -316,23 +314,6 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 51CAF0D65C0347913F051B6B /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -481,6 +462,7 @@ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -663,6 +645,7 @@ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -685,6 +668,7 @@ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -731,12 +715,14 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + /* Begin XCLocalSwiftPackageReference section */ 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { isa = XCLocalSwiftPackageReference; relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; }; /* End XCLocalSwiftPackageReference section */ + /* Begin XCSwiftPackageProductDependency section */ 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { isa = XCSwiftPackageProductDependency; diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..7462322 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,42 @@ +{ + "originHash" : "57ef012bc52967b81c4acfe75c7aa726cca1989d6abec1c186cfe85e2f9098fd", + "pins" : [ + { + "identity" : "nwwebsocket", + "kind" : "remoteSourceControl", + "location" : "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/pusher/NWWebSocket.git", + "state" : { + "revision" : "1e545fcb53966272fc042aa17ae932f11239e00f", + "version" : "0.5.4" + } + }, + { + "identity" : "paystack-sdk-ios", + "kind" : "remoteSourceControl", + "location" : "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/PaystackHQ/paystack-sdk-ios.git", + "state" : { + "revision" : "f1311e5f80fa36b8b0625581cc10860157998f61", + "version" : "0.0.4" + } + }, + { + "identity" : "pusher-websocket-swift", + "kind" : "remoteSourceControl", + "location" : "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/pusher/pusher-websocket-swift.git", + "state" : { + "revision" : "886341f9dad453c9822f2525136ee2006a6c3c9e", + "version" : "10.1.5" + } + }, + { + "identity" : "tweetnacl-swiftwrap", + "kind" : "remoteSourceControl", + "location" : "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/bitmark-inc/tweetnacl-swiftwrap", + "state" : { + "revision" : "f8fd111642bf2336b11ef9ea828510693106e954", + "version" : "1.1.0" + } + } + ], + "version" : 3 +} diff --git a/example/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/example/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..7462322 --- /dev/null +++ b/example/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,42 @@ +{ + "originHash" : "57ef012bc52967b81c4acfe75c7aa726cca1989d6abec1c186cfe85e2f9098fd", + "pins" : [ + { + "identity" : "nwwebsocket", + "kind" : "remoteSourceControl", + "location" : "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/pusher/NWWebSocket.git", + "state" : { + "revision" : "1e545fcb53966272fc042aa17ae932f11239e00f", + "version" : "0.5.4" + } + }, + { + "identity" : "paystack-sdk-ios", + "kind" : "remoteSourceControl", + "location" : "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/PaystackHQ/paystack-sdk-ios.git", + "state" : { + "revision" : "f1311e5f80fa36b8b0625581cc10860157998f61", + "version" : "0.0.4" + } + }, + { + "identity" : "pusher-websocket-swift", + "kind" : "remoteSourceControl", + "location" : "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/pusher/pusher-websocket-swift.git", + "state" : { + "revision" : "886341f9dad453c9822f2525136ee2006a6c3c9e", + "version" : "10.1.5" + } + }, + { + "identity" : "tweetnacl-swiftwrap", + "kind" : "remoteSourceControl", + "location" : "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/bitmark-inc/tweetnacl-swiftwrap", + "state" : { + "revision" : "f8fd111642bf2336b11ef9ea828510693106e954", + "version" : "1.1.0" + } + } + ], + "version" : 3 +} diff --git a/example/lib/main.dart b/example/lib/main.dart index 89dfc47..4283c02 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,10 +1,9 @@ import 'package:flutter/material.dart'; import 'dart:async'; +import 'dart:developer'; import 'package:flutter/services.dart'; -import 'package:paystack_flutter/paystack_flutter.dart'; import 'package:paystack_flutter/paystack.dart'; -import 'package:paystack_flutter/payment_sheet.dart'; void main() { runApp(const MyApp()); @@ -18,58 +17,55 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State { - String _platformVersion = 'Unknown'; - final _paymentSheet = PaymentSheet(); - final _paystackFlutterPlugin = PaystackFlutter(); + String _paymentReference = ""; + final _publicKey = "pk_test_xxxxxx"; + final _accessCode = "2g3pyob7ey9dbtj"; + final _paystack = Paystack(); @override void initState() { super.initState(); - initPlatformState(); - - Paystack() - // .setPublicKey("pk_test_xxxx") - .enableLogging(true) - .build(); + initialize(_publicKey); } - // Platform messages are asynchronous, so we initialize in an async method. - Future initPlatformState() async { - String platformVersion; - // Platform messages may fail, so we use a try/catch PlatformException. - // We also handle the message potentially returning null. + Future initialize(String publicKey) async { try { - platformVersion = await _paystackFlutterPlugin.getPlatformVersion() ?? - 'Unknown platform version'; - } on PlatformException { - platformVersion = 'Failed to get platform version.'; + final response = await _paystack.initialize(publicKey, true); + log(response as String); + } on PlatformException catch (e) { + print(e); } - - // If the widget was removed from the tree while the asynchronous platform - // message was in flight, we want to discard the reply rather than calling - // setState to update our non-existent appearance. - if (!mounted) return; - - setState(() { - _platformVersion = platformVersion; - }); } - Future launchPaystack() async { - String response; + Future launch() async { + String reference = ""; try { - response = - await _paymentSheet.launch("access_codes") ?? "Cannot launch"; - } on PlatformException { - response = 'Failed to launch SDK.'; + final response = await _paystack.launch(_accessCode); + if (response.status) { + reference = response.reference; + print(reference); + // _displayToast(context, reference); + } else { + print(response.message); + } + } on PlatformException catch (e) { + print(e); } - print(response); + setState(() { + _paymentReference = reference; + }); } - // void _triggerMethodChannel() { - // launchPaystack(); - // } + void _displayToast(BuildContext context, String text) { + final scaffold = ScaffoldMessenger.of(context); + scaffold.showSnackBar( + SnackBar( + content: Text(text), + action: SnackBarAction(label: "SDK", onPressed: scaffold.hideCurrentSnackBar), + ) + ); + } @override Widget build(BuildContext context) { @@ -82,10 +78,7 @@ class _MyAppState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( - onPressed: launchPaystack, - child: const Text('Make Payment')), - const SizedBox(width: 16), - Text('Running on: $_platformVersion\n'), + onPressed: launch, child: const Text('Make Payment')), ], )), ); diff --git a/ios/.gitignore b/ios/.gitignore index 0c88507..bd7e788 100644 --- a/ios/.gitignore +++ b/ios/.gitignore @@ -7,6 +7,9 @@ *.swp profile +/paystack_flutter/.build/ +/paystack_flutter/.swiftpm/ + DerivedData/ build/ GeneratedPluginRegistrant.h diff --git a/ios/Classes/PaystackFlutterPlugin.swift b/ios/Classes/PaystackFlutterPlugin.swift deleted file mode 100644 index bb71a10..0000000 --- a/ios/Classes/PaystackFlutterPlugin.swift +++ /dev/null @@ -1,32 +0,0 @@ -import Flutter -import UIKit - -public class PaystackFlutterPlugin: NSObject, FlutterPlugin { - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "com.paystack.flutter", binaryMessenger: registrar.messenger()) - let instance = PaystackFlutterPlugin() - registrar.addMethodCallDelegate(instance, channel: channel) - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - let arguments = call.arguments as? [String: Any] - switch call.method { - case "getPlatformVersion": - result("iOS " + UIDevice.current.systemVersion) - case "build": - initializeSDK(publicKey: "something", logging: true) - result("Setup implementation in progress...") - case "launch": - print("Calling Launch SDK") - result(arguments?["accessCode"] as? String) -// result("Launch implementation in progress...") - default: - result(FlutterMethodNotImplemented) - } - } - - private func initializeSDK(publicKey: String, logging: Bool) { - // paystack = try? PaystackBuilder.newInstance.setKey(publicKey) - - } -} diff --git a/ios/paystack_flutter.podspec b/ios/paystack_flutter.podspec deleted file mode 100644 index 64ebe46..0000000 --- a/ios/paystack_flutter.podspec +++ /dev/null @@ -1,23 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint paystack_flutter.podspec` to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'paystack_flutter' - s.version = '0.0.1' - s.summary = 'A new Flutter plugin project.' - s.description = <<-DESC -A new Flutter plugin project. - DESC - s.homepage = 'http://example.com' - s.license = { :file => '../LICENSE' } - s.author = { 'Your Company' => 'email@example.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.dependency 'Flutter' - s.platform = :ios, '12.0' - - # Flutter.framework does not contain a i386 slice. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } - s.swift_version = '5.0' -end diff --git a/ios/paystack_flutter/Package.resolved b/ios/paystack_flutter/Package.resolved new file mode 100644 index 0000000..240d3ac --- /dev/null +++ b/ios/paystack_flutter/Package.resolved @@ -0,0 +1,42 @@ +{ + "originHash" : "6e8cb66d6916469988846e004530ed258adc3f099a11f76a76553f198b4e93fa", + "pins" : [ + { + "identity" : "nwwebsocket", + "kind" : "remoteSourceControl", + "location" : "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/pusher/NWWebSocket.git", + "state" : { + "revision" : "1e545fcb53966272fc042aa17ae932f11239e00f", + "version" : "0.5.4" + } + }, + { + "identity" : "paystack-sdk-ios", + "kind" : "remoteSourceControl", + "location" : "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/PaystackHQ/paystack-sdk-ios.git", + "state" : { + "revision" : "f1311e5f80fa36b8b0625581cc10860157998f61", + "version" : "0.0.4" + } + }, + { + "identity" : "pusher-websocket-swift", + "kind" : "remoteSourceControl", + "location" : "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/pusher/pusher-websocket-swift.git", + "state" : { + "revision" : "886341f9dad453c9822f2525136ee2006a6c3c9e", + "version" : "10.1.5" + } + }, + { + "identity" : "tweetnacl-swiftwrap", + "kind" : "remoteSourceControl", + "location" : "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/bitmark-inc/tweetnacl-swiftwrap", + "state" : { + "revision" : "f8fd111642bf2336b11ef9ea828510693106e954", + "version" : "1.1.0" + } + } + ], + "version" : 3 +} diff --git a/ios/paystack_flutter/Package.swift b/ios/paystack_flutter/Package.swift new file mode 100644 index 0000000..d33e2f8 --- /dev/null +++ b/ios/paystack_flutter/Package.swift @@ -0,0 +1,27 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "paystack_flutter", + platforms: [ + .iOS(.v14), + ], + products: [ + .library(name: "paystack-flutter", targets: ["paystack_flutter"]) + ], + dependencies: [ + .package(url: "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/PaystackHQ/paystack-sdk-ios.git", .upToNextMajor(from: "0.0.4")), + ], + targets: [ + .target( + name: "paystack_flutter", + dependencies: [ + .product(name: "PaystackCore", package: "paystack-sdk-ios"), + .product(name: "PaystackUI", package: "paystack-sdk-ios") + ], + resources: [] + ) + ] +) diff --git a/ios/Assets/.gitkeep b/ios/paystack_flutter/Sources/paystack_flutter/.gitkeep similarity index 100% rename from ios/Assets/.gitkeep rename to ios/paystack_flutter/Sources/paystack_flutter/.gitkeep diff --git a/ios/paystack_flutter/Sources/paystack_flutter/PaystackFlutterPlugin.swift b/ios/paystack_flutter/Sources/paystack_flutter/PaystackFlutterPlugin.swift new file mode 100644 index 0000000..0452922 --- /dev/null +++ b/ios/paystack_flutter/Sources/paystack_flutter/PaystackFlutterPlugin.swift @@ -0,0 +1,114 @@ +import Flutter +import UIKit +import PaystackCore +import PaystackUI + +public class PaystackFlutterPlugin: NSObject, FlutterPlugin { + private var paystack: Paystack? + private var result: FlutterResult? + + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "com.paystack.flutter", binaryMessenger: registrar.messenger()) + let instance = PaystackFlutterPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + self.result = result + switch call.method { + case "initialize": + guard let args = call.arguments as? [String: Any], + let publicKey = args["publicKey"] as? String else { + result(FlutterError(code: "INVALID_ARGUMENTS", + message: "Missing public key", + details: nil)) + return + } + let logging = args["enableLogging"] as? Bool ?? false + initialize(publicKey: publicKey, logging: logging) + case "launch": + guard let args = call.arguments as? [String: Any], + let accessCode = args["accessCode"] as? String else { + result(FlutterError(code: "INVALID_ARGUMENTS", + message: "Missing access code", + details: nil)) + return + } + launch(accessCode: accessCode) + default: + result(FlutterMethodNotImplemented) + } + } + + private func initialize(publicKey: String, logging: Bool) { + do { + let instance = PaystackBuilder + .newInstance + .setKey(publicKey) + + if logging { + instance.enableLogging() + } + + paystack = try instance.build() + self.result?(true) + } catch { + self.result?(FlutterError(code: "INITIALIZATION_ERROR", + message: error.localizedDescription, + details: nil)) + } + } + + private func launch(accessCode: String) { + guard let paystack = paystack else { + self.result?(FlutterError(code: "NOT_INITIALIZED", + message: "Paystack not initialized", + details: nil)) + return + } + + guard #available(iOS 14.0, *) else { + self.result?(FlutterError(code: "UNSUPPORTED_VERSION", + message: "Paystack UI requires iOS 14.0 or later", + details: nil)) + return + } + + guard let viewController = UIApplication.shared.keyWindow?.rootViewController else { + self.result?(FlutterError(code: "NO_VIEW_CONTROLLER", + message: "Unable to present payment UI", + details: nil)) + return + } + + paystack.presentChargeUI( + on: viewController, + accessCode: accessCode, + onComplete: { [weak self] transactionResult in + self?.paymentDone(transactionResult) + }) + } + + private func paymentDone(_ transactionResult: TransactionResult) { + switch (transactionResult) { + case .completed(let details): + self.result?([ + "status": true, + "message": "Transaction successful", + "reference": details.reference + ]) + case .cancelled: + self.result?([ + "status": false, + "message": "Transaction cancelled", + "reference": "" + ]) + case .error(error: let error, reference: let reference): + self.result?([ + "status": false, + "message": error.message, + "reference": "" + ]) + } + } +} diff --git a/ios/paystack_flutter/paystack_flutter.podspec b/ios/paystack_flutter/paystack_flutter.podspec new file mode 100644 index 0000000..8b84d98 --- /dev/null +++ b/ios/paystack_flutter/paystack_flutter.podspec @@ -0,0 +1,26 @@ +Pod::Spec.new do |s| + s.name = 'paystack_flutter' + s.version = '0.0.1' + s.summary = 'A Paystack Flutter plugin for accepting payments.' + s.description = <<-DESC +A Paystack Flutter plugin for accepting payments. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Paystack Developer Relations' => 'devrel@paystack.com' } + s.source = { :path => '.' } + # s.source_files = 'Classes/**/*' + s.platform = :ios, '14.0' + s.source_files = 'sample_sdk/Sources/sample_sdk/**/*.swift' + s.dependency 'Flutter' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + + # SPM config + s.swift_version = '5.9' + s.preserve_paths = 'Package.swift', '.swiftpm/**/*' + s.prepare_command = <<-CMD + swift pacakge resolve + CMD +end diff --git a/lib/payment_sheet.dart b/lib/payment_sheet.dart deleted file mode 100644 index 6f566e5..0000000 --- a/lib/payment_sheet.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'paystack_flutter_platform_interface.dart'; - -class PaymentSheet { - Future launch(String accessCode) { - return PaystackFlutterPlatform.instance.launch(accessCode); - } -} diff --git a/lib/paystack.dart b/lib/paystack.dart index 20d7ed7..248de7b 100644 --- a/lib/paystack.dart +++ b/lib/paystack.dart @@ -1,28 +1,2 @@ -import 'package:flutter/foundation.dart'; - -import 'paystack_flutter_platform_interface.dart'; - -class Paystack { - String _publicKey = ""; - bool _logging = false; - - Paystack setPublicKey(String publicKey) { - _publicKey = publicKey; - return this; - } - - Paystack enableLogging(bool logging) { - _logging = logging; - return this; - } - - Paystack build() { - _buildInternal(_publicKey, _logging); - return this; - } - - @protected - Future _buildInternal(String publicKey, bool enableLogging) { - return PaystackFlutterPlatform.instance.build(publicKey, enableLogging); - } -} +library Paystack; +export "src/paystack_flutter.dart" show Paystack; \ No newline at end of file diff --git a/lib/paystack_flutter.dart b/lib/paystack_flutter.dart deleted file mode 100644 index a91e516..0000000 --- a/lib/paystack_flutter.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'paystack_flutter_platform_interface.dart'; - -// TODO: Delete file after iOS integration -class PaystackFlutter { - Future getPlatformVersion() { - return PaystackFlutterPlatform.instance.getPlatformVersion(); - } - - // Future build(String publicKey, bool enableLogging) { - // return PaystackFlutterPlatform.instance.build(publicKey, enableLogging); - // } - - // Future launch(String accessCode) { - // return PaystackFlutterPlatform.instance.launch(accessCode); - // } -} diff --git a/lib/paystack_flutter_method_channel.dart b/lib/paystack_flutter_method_channel.dart deleted file mode 100644 index 55e84c1..0000000 --- a/lib/paystack_flutter_method_channel.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; - -import 'paystack_flutter_platform_interface.dart'; - -class MethodChannelPaystackFlutter extends PaystackFlutterPlatform { - @visibleForTesting - final methodChannel = const MethodChannel('com.paystack.flutter'); - - @override - Future getPlatformVersion() async { - final version = - await methodChannel.invokeMethod('getPlatformVersion'); - return version; - } - - @override - Future build(String publicKey, bool enableLogging) async { - final response = await methodChannel.invokeMethod( - 'build', {'publicKey': publicKey, 'enableLogging': enableLogging}); - - return response; - } - - @override - Future launch(String accessCode) async { - final response = await methodChannel - .invokeMethod('launch', {'accessCode': accessCode}); - - return response; - } -} diff --git a/lib/src/models.dart b/lib/src/models.dart new file mode 100644 index 0000000..afd350e --- /dev/null +++ b/lib/src/models.dart @@ -0,0 +1,30 @@ +class TransactionResponse { + final bool status; + final String message; + final String reference; + + TransactionResponse({ + required this.status, + required this.message, + required this.reference + }); + + factory TransactionResponse.fromMap(Map map) { + return TransactionResponse( + status: map['status'] as bool, + message: map['message'] as String, + reference: map['reference'] as String, + ); + } +} + +class PaystackException implements Exception { + final String message; + final String code; + final dynamic details; + + PaystackException({required this.message, required this.code, this.details}); + + @override + String toString() => 'PaystackException: $message (code: $code)'; +} diff --git a/lib/src/paystack_flutter.dart b/lib/src/paystack_flutter.dart new file mode 100644 index 0000000..5727338 --- /dev/null +++ b/lib/src/paystack_flutter.dart @@ -0,0 +1,13 @@ +import 'paystack_flutter_platform_interface.dart'; +import 'models.dart'; + +class Paystack { + Future initialize(String publicKey, bool enableLogging) { + return PaystackFlutterPlatform.instance + .initialize(publicKey, enableLogging); + } + + Future launch(String accessCode) { + return PaystackFlutterPlatform.instance.launch(accessCode); + } +} \ No newline at end of file diff --git a/lib/src/paystack_flutter_method_channel.dart b/lib/src/paystack_flutter_method_channel.dart new file mode 100644 index 0000000..bc2b3dd --- /dev/null +++ b/lib/src/paystack_flutter_method_channel.dart @@ -0,0 +1,40 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'paystack_flutter_platform_interface.dart'; +import 'models.dart'; + +class MethodChannelPaystackFlutter extends PaystackFlutterPlatform { + @visibleForTesting + final methodChannel = const MethodChannel('com.paystack.flutter'); + + @override + Future initialize(String publicKey, bool enableLogging) async { + try { + final response = await methodChannel.invokeMethod('initialize', + {'publicKey': publicKey, 'enableLogging': enableLogging}); + + return response!; + } on PlatformException catch (e) { + throw PaystackException( + message: e.message ?? 'Failed to initialize Paystack', + code: e.code, + details: e.details); + } + } + + @override + Future launch(String accessCode) async { + try { + final response = await methodChannel + .invokeMethod>('launch', {'accessCode': accessCode}); + + return TransactionResponse.fromMap(response as Map); + } on PlatformException catch (e) { + throw PaystackException( + message: e.message ?? 'Transaction Failed', + code: e.code, + details: e.details); + } + } +} diff --git a/lib/paystack_flutter_platform_interface.dart b/lib/src/paystack_flutter_platform_interface.dart similarity index 75% rename from lib/paystack_flutter_platform_interface.dart rename to lib/src/paystack_flutter_platform_interface.dart index 0fe38e9..2d07220 100644 --- a/lib/paystack_flutter_platform_interface.dart +++ b/lib/src/paystack_flutter_platform_interface.dart @@ -1,6 +1,7 @@ import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'paystack_flutter_method_channel.dart'; +import 'models.dart'; abstract class PaystackFlutterPlatform extends PlatformInterface { PaystackFlutterPlatform() : super(token: _token); @@ -14,15 +15,11 @@ abstract class PaystackFlutterPlatform extends PlatformInterface { _instance = instance; } - Future getPlatformVersion() { - throw UnimplementedError('platformVersion() has not been implemented.'); - } - - Future build(String publicKey, bool enableLogging) { + Future initialize(String publicKey, bool enableLogging) { throw UnimplementedError('build() has not been implemented'); } - Future launch(String accessCode) { + Future launch(String accessCode) { throw UnimplementedError('launch() has not been implemented'); } } diff --git a/test/paystack_flutter_method_channel_test.dart b/test/paystack_flutter_method_channel_test.dart index 1b78a4e..1c328f1 100644 --- a/test/paystack_flutter_method_channel_test.dart +++ b/test/paystack_flutter_method_channel_test.dart @@ -1,6 +1,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:paystack_flutter/paystack_flutter_method_channel.dart'; +import 'package:paystack_flutter/src/paystack_flutter_method_channel.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -22,6 +22,6 @@ void main() { }); test('getPlatformVersion', () async { - expect(await platform.getPlatformVersion(), '42'); + // expect(await platform.getPlatformVersion(), '42'); }); } diff --git a/test/paystack_test.dart b/test/paystack_test.dart index b0d3be6..b632f30 100644 --- a/test/paystack_test.dart +++ b/test/paystack_test.dart @@ -1,8 +1,7 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:paystack_flutter/paystack_flutter.dart'; -import 'package:paystack_flutter/payment_sheet.dart'; -import 'package:paystack_flutter/paystack_flutter_platform_interface.dart'; -import 'package:paystack_flutter/paystack_flutter_method_channel.dart'; +import 'package:paystack_flutter/src/models.dart'; +import 'package:paystack_flutter/src/paystack_flutter_platform_interface.dart'; +import 'package:paystack_flutter/src/paystack_flutter_method_channel.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; class MockPaystackFlutterPlatform @@ -15,10 +14,17 @@ class MockPaystackFlutterPlatform Future build(String publicKey, bool enableLogging) { return Future.value("Coming soon..."); } + + @override + Future initialize(String publicKey, bool enableLogging) { + // TODO: implement initialize + throw UnimplementedError(); + } @override - Future launch(String accessCode) { - return Future.value('Launched'); + Future launch(String accessCode) { + // TODO: implement launch + throw UnimplementedError(); } } @@ -31,20 +37,20 @@ void main() { }); test('getPlatformVersion', () async { - PaystackFlutter paystackFlutterPlugin = PaystackFlutter(); + // PaystackFlutter paystackFlutterPlugin = PaystackFlutter(); MockPaystackFlutterPlatform fakePlatform = MockPaystackFlutterPlatform(); PaystackFlutterPlatform.instance = fakePlatform; - expect(await paystackFlutterPlugin.getPlatformVersion(), '42'); + // expect(await paystackFlutterPlugin.getPlatformVersion(), '42'); }); // The unit test is actually not worth it! test('launch', () async { - PaymentSheet paymentSheet = PaymentSheet(); + // PaymentSheet paymentSheet = PaymentSheet(); MockPaystackFlutterPlatform fakePlatform = MockPaystackFlutterPlatform(); PaystackFlutterPlatform.instance = fakePlatform; - expect(await paymentSheet.launch("accessCode"), "Launched"); + // expect(await paymentSheet.launch("accessCode"), "Launched"); }); }