From 55d5e43187660bb448a58ed715ea4f089fa0bbba Mon Sep 17 00:00:00 2001 From: Damilola Odujoko Date: Fri, 31 Jan 2025 13:52:43 +0100 Subject: [PATCH 1/4] Feat: Enable SPM and consume iOS SDK --- .../paystack_flutter/PaystackFlutterPlugin.kt | 10 +- example/ios/Flutter/AppFrameworkInfo.plist | 40 +++---- example/ios/Podfile.lock | 6 -- example/ios/Runner.xcodeproj/project.pbxproj | 38 +++---- .../xcshareddata/swiftpm/Package.resolved | 42 ++++++++ .../xcshareddata/swiftpm/Package.resolved | 42 ++++++++ example/lib/main.dart | 78 +++++++------- ios/.gitignore | 3 + ios/Classes/PaystackFlutterPlugin.swift | 32 ------ ios/paystack_flutter.podspec | 23 ---- ios/paystack_flutter/Package.resolved | 42 ++++++++ ios/paystack_flutter/Package.swift | 27 +++++ .../Sources/paystack_flutter}/.gitkeep | 0 .../PaystackFlutterPlugin.swift | 102 ++++++++++++++++++ ios/paystack_flutter/paystack_flutter.podspec | 26 +++++ lib/payment_sheet.dart | 7 -- lib/paystack.dart | 26 ++--- lib/paystack_flutter.dart | 16 --- lib/paystack_flutter_method_channel.dart | 18 ++-- lib/paystack_flutter_platform_interface.dart | 6 +- 20 files changed, 374 insertions(+), 210 deletions(-) create mode 100644 example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 example/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved delete mode 100644 ios/Classes/PaystackFlutterPlugin.swift delete mode 100644 ios/paystack_flutter.podspec create mode 100644 ios/paystack_flutter/Package.resolved create mode 100644 ios/paystack_flutter/Package.swift rename ios/{Assets => paystack_flutter/Sources/paystack_flutter}/.gitkeep (100%) create mode 100644 ios/paystack_flutter/Sources/paystack_flutter/PaystackFlutterPlugin.swift create mode 100644 ios/paystack_flutter/paystack_flutter.podspec delete mode 100644 lib/payment_sheet.dart delete mode 100644 lib/paystack_flutter.dart 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..06e3b3c 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 @@ -21,7 +20,7 @@ class PaystackFlutterPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { private lateinit var paymentSheet: PaymentSheet private var activity: FlutterFragmentActivity? = null - private fun initializePaystack(publicKey: String, enableLogging: Boolean) { + private fun initialize(publicKey: String, enableLogging: Boolean) { Paystack .builder() .setPublicKey(publicKey) @@ -55,9 +54,8 @@ class PaystackFlutterPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { // 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( + "initialize" -> { + initialize( call.argument("publicKey") ?: "", call.argument("enableLogging") ?: false) result.success("Initiated") @@ -76,12 +74,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..1324405 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -2,9 +2,7 @@ import 'package:flutter/material.dart'; import 'dart:async'; 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,59 +16,69 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State { - String _platformVersion = 'Unknown'; - final _paymentSheet = PaymentSheet(); - final _paystackFlutterPlugin = PaystackFlutter(); + String _platformVersion = "Unknown"; + String _paymentReference = ""; + String _publicKey = "pk_test_xxxxxx"; + String _accessCode = "fhnvmojgrpcdp98"; + final _paystack = Paystack(); @override void initState() { super.initState(); - initPlatformState(); - - Paystack() - // .setPublicKey("pk_test_xxxx") - .enableLogging(true) - .build(); + // initPlatformState(); + 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 initPlatformState() async { + // String platformVersion; + // // Platform messages may fail, so we use a try/catch PlatformException. + // // We also handle the message potentially returning null. + // try { + // platformVersion = await _paystack.getPlatformVersion() ?? + // 'Unknown platform version'; + // } on PlatformException { + // platformVersion = 'Failed to get platform version.'; + // } + + // // 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 initialize(String publicKey) async { + String response; try { - platformVersion = await _paystackFlutterPlugin.getPlatformVersion() ?? - 'Unknown platform version'; + response = await _paystack.initialize(publicKey, true) ?? + "Cannot init"; } on PlatformException { - platformVersion = 'Failed to get platform version.'; + response = 'Failed to set up SDK.'; } - // 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; - }); + print(response); } - Future launchPaystack() async { + Future launch() async { String response; try { - response = - await _paymentSheet.launch("access_codes") ?? "Cannot launch"; + response = await _paystack.launch(_accessCode) + ?? "Cannot launch"; } on PlatformException { response = 'Failed to launch SDK.'; } print(response); + + setState(() { + _paymentReference = response; + }); } - // void _triggerMethodChannel() { - // launchPaystack(); - // } - @override Widget build(BuildContext context) { return MaterialApp( @@ -82,10 +90,8 @@ class _MyAppState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( - onPressed: launchPaystack, + onPressed: launch, child: const Text('Make Payment')), - const SizedBox(width: 16), - Text('Running on: $_platformVersion\n'), ], )), ); 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..17feb27 --- /dev/null +++ b/ios/paystack_flutter/Sources/paystack_flutter/PaystackFlutterPlugin.swift @@ -0,0 +1,102 @@ +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) { + 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 + } + initialize(publicKey: publicKey) + 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) { + do { + paystack = try PaystackBuilder + .newInstance + .setKey(publicKey) + .build() + result?("true") + } catch { + result?(FlutterError(code: "INITIALIZATION_ERROR", + message: error.localizedDescription, + details: nil)) + } + } + + private func launch(accessCode: String) { + guard let paystack = paystack else { + result?(FlutterError(code: "NOT_INITIALIZED", + message: "Paystack not initialized", + details: nil)) + return + } + + guard #available(iOS 14.0, *) else { + 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 { + 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(_ result: TransactionResult) { + switch (result) { + case .completed(let details): + self.result?([ + "status": "success", + "reference": details.reference + ]) + case .cancelled: + self.result?([ + "status": "cancelled" + ]) + case .error(error: let error, reference: let reference): + self.result?(FlutterError(code: "TRANSACTION_ERROR", + message: error.message, + details: ["reference": 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..dc9dbd5 100644 --- a/lib/paystack.dart +++ b/lib/paystack.dart @@ -1,28 +1,14 @@ -import 'package:flutter/foundation.dart'; - import 'paystack_flutter_platform_interface.dart'; +// TODO: Handle responses +// TODO: Consider exposing with builder pattern 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; + Future initialize(String publicKey, bool enableLogging) { + return PaystackFlutterPlatform.instance.initialize(publicKey, enableLogging); } - @protected - Future _buildInternal(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.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 index 55e84c1..7bfe54e 100644 --- a/lib/paystack_flutter_method_channel.dart +++ b/lib/paystack_flutter_method_channel.dart @@ -3,29 +3,23 @@ import 'package:flutter/services.dart'; import 'paystack_flutter_platform_interface.dart'; +// TODO: Handle responses 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}); + Future initialize(String publicKey, bool enableLogging) async { + final response = await methodChannel.invokeMethod('initialize', + {'publicKey': publicKey, 'enableLogging': enableLogging}); return response; } @override Future launch(String accessCode) async { - final response = await methodChannel - .invokeMethod('launch', {'accessCode': accessCode}); + final response = await methodChannel.invokeMethod('launch', + {'accessCode': accessCode}); return response; } diff --git a/lib/paystack_flutter_platform_interface.dart b/lib/paystack_flutter_platform_interface.dart index 0fe38e9..a8e32fa 100644 --- a/lib/paystack_flutter_platform_interface.dart +++ b/lib/paystack_flutter_platform_interface.dart @@ -14,11 +14,7 @@ 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'); } From f9f7e7163ca1e44abeb3d1e7678c84a5b2a953bc Mon Sep 17 00:00:00 2001 From: Damilola Odujoko Date: Mon, 3 Feb 2025 13:15:23 +0100 Subject: [PATCH 2/4] Feat: Interface unification --- .../paystack_flutter/PaystackFlutterPlugin.kt | 103 ++++++++++++------ example/lib/main.dart | 72 +++++------- .../PaystackFlutterPlugin.swift | 31 ++++-- lib/models.dart | 30 +++++ lib/paystack.dart | 12 +- lib/paystack_flutter_method_channel.dart | 37 +++++-- lib/paystack_flutter_platform_interface.dart | 5 +- 7 files changed, 187 insertions(+), 103 deletions(-) create mode 100644 lib/models.dart 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 06e3b3c..d08043e 100644 --- a/android/src/main/kotlin/com/paystack/paystack_flutter/PaystackFlutterPlugin.kt +++ b/android/src/main/kotlin/com/paystack/paystack_flutter/PaystackFlutterPlugin.kt @@ -19,55 +19,92 @@ class PaystackFlutterPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { private lateinit var channel : MethodChannel private lateinit var paymentSheet: PaymentSheet private var activity: FlutterFragmentActivity? = null - - private fun initialize(publicKey: String, enableLogging: Boolean) { - Paystack - .builder() - .setPublicKey(publicKey) - .setLoggingEnabled(enableLogging) - .build() - } - - private fun launch(accessCode: String) { - paymentSheet.launch(accessCode) - } - - // 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 - } - } - - return response - } + private var pendingResult: Result? = null override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { channel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.paystack.flutter") channel.setMethodCallHandler(this) } - // TODO: Return object in result as opposed to String override fun onMethodCall(call: MethodCall, result: Result) { when (call.method) { "initialize" -> { - initialize( - call.argument("publicKey") ?: "", - call.argument("enableLogging") ?: false) - result.success("Initiated") + 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" -> { - launch(call.argument("accessCode") ?: "") - result.success("Launch called") + val accessCode = call.argument("accessCode") + if(accessCode.isNullOrEmpty()) { + result.error("INVALID_ARGUMENTS", "Missing access code", null) + return + } + launch(accessCode) } else -> result.notImplemented() } } + 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) + } + } + + 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 + } + + 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 + )) + } + 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 "" + )) + } + } + pendingResult = null + } + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { channel.setMethodCallHandler(null) } diff --git a/example/lib/main.dart b/example/lib/main.dart index 1324405..477679b 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -16,69 +16,56 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State { - String _platformVersion = "Unknown"; String _paymentReference = ""; - String _publicKey = "pk_test_xxxxxx"; - String _accessCode = "fhnvmojgrpcdp98"; + final _publicKey = "pk_test_xxxxxx"; + final _accessCode = "2g3pyob7ey9dbtj"; final _paystack = Paystack(); @override void initState() { super.initState(); - // initPlatformState(); 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. - // try { - // platformVersion = await _paystack.getPlatformVersion() ?? - // 'Unknown platform version'; - // } on PlatformException { - // platformVersion = 'Failed to get platform version.'; - // } - - // // 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 initialize(String publicKey) async { - String response; try { - response = await _paystack.initialize(publicKey, true) ?? - "Cannot init"; - } on PlatformException { - response = 'Failed to set up SDK.'; + final response = await _paystack.initialize(publicKey, true); + print(response); + } on PlatformException catch (e) { + print(e); } - - print(response); } Future launch() async { - String response; + String reference = ""; try { - response = await _paystack.launch(_accessCode) - ?? "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 = response; + _paymentReference = reference; }); } + 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) { return MaterialApp( @@ -90,8 +77,7 @@ class _MyAppState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( - onPressed: launch, - child: const Text('Make Payment')), + onPressed: launch, child: const Text('Make Payment')), ], )), ); diff --git a/ios/paystack_flutter/Sources/paystack_flutter/PaystackFlutterPlugin.swift b/ios/paystack_flutter/Sources/paystack_flutter/PaystackFlutterPlugin.swift index 17feb27..ac470e7 100644 --- a/ios/paystack_flutter/Sources/paystack_flutter/PaystackFlutterPlugin.swift +++ b/ios/paystack_flutter/Sources/paystack_flutter/PaystackFlutterPlugin.swift @@ -23,7 +23,8 @@ public class PaystackFlutterPlugin: NSObject, FlutterPlugin { details: nil)) return } - initialize(publicKey: publicKey) + 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 { @@ -38,13 +39,18 @@ public class PaystackFlutterPlugin: NSObject, FlutterPlugin { } } - private func initialize(publicKey: String) { + private func initialize(publicKey: String, logging: Bool) { do { - paystack = try PaystackBuilder + let instance = PaystackBuilder .newInstance .setKey(publicKey) - .build() - result?("true") + + if logging { + instance.enableLogging() + } + + paystack = try instance.build() + result?(true) } catch { result?(FlutterError(code: "INITIALIZATION_ERROR", message: error.localizedDescription, @@ -86,17 +92,22 @@ public class PaystackFlutterPlugin: NSObject, FlutterPlugin { switch (result) { case .completed(let details): self.result?([ - "status": "success", + "status": true, + "message": "Transaction successful", "reference": details.reference ]) case .cancelled: self.result?([ - "status": "cancelled" + "status": false, + "message": "Transaction cancelled", + "reference": "" ]) case .error(error: let error, reference: let reference): - self.result?(FlutterError(code: "TRANSACTION_ERROR", - message: error.message, - details: ["reference": reference ?? ""])) + self.result?([ + "status": false, + "message": error.message, + "reference": "" + ]) } } } diff --git a/lib/models.dart b/lib/models.dart new file mode 100644 index 0000000..afd350e --- /dev/null +++ b/lib/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/paystack.dart b/lib/paystack.dart index dc9dbd5..1ce5fc4 100644 --- a/lib/paystack.dart +++ b/lib/paystack.dart @@ -1,14 +1,14 @@ import 'paystack_flutter_platform_interface.dart'; +import 'models.dart'; -// TODO: Handle responses -// TODO: Consider exposing with builder pattern -class Paystack { - Future initialize(String publicKey, bool enableLogging) { - return PaystackFlutterPlatform.instance.initialize(publicKey, enableLogging); +class Paystack { + Future initialize(String publicKey, bool enableLogging) { + return PaystackFlutterPlatform.instance + .initialize(publicKey, enableLogging); } - Future launch(String accessCode) { + 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 index 7bfe54e..599802b 100644 --- a/lib/paystack_flutter_method_channel.dart +++ b/lib/paystack_flutter_method_channel.dart @@ -2,25 +2,44 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'paystack_flutter_platform_interface.dart'; +import 'models.dart'; -// TODO: Handle responses class MethodChannelPaystackFlutter extends PaystackFlutterPlatform { @visibleForTesting final methodChannel = const MethodChannel('com.paystack.flutter'); @override - Future initialize(String publicKey, bool enableLogging) async { - final response = await methodChannel.invokeMethod('initialize', - {'publicKey': publicKey, 'enableLogging': enableLogging}); + Future initialize(String publicKey, bool enableLogging) async { + try { + final response = await methodChannel.invokeMethod('initialize', { + 'publicKey': publicKey, + 'enableLogging': enableLogging + }); - return response; + return response as bool; + } 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 { - final response = await methodChannel.invokeMethod('launch', - {'accessCode': accessCode}); + Future launch(String accessCode) async { + try { + final response = await methodChannel.invokeMethod('launch', { + 'accessCode': accessCode + }); - return response; + 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/paystack_flutter_platform_interface.dart index a8e32fa..2d07220 100644 --- a/lib/paystack_flutter_platform_interface.dart +++ b/lib/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,11 +15,11 @@ abstract class PaystackFlutterPlatform extends PlatformInterface { _instance = instance; } - Future initialize(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'); } } From 23981026bd025d0bcfc9b65f0db9bf429c61466a Mon Sep 17 00:00:00 2001 From: Damilola Odujoko Date: Wed, 5 Feb 2025 15:02:54 +0100 Subject: [PATCH 3/4] Expose required package --- .../paystack_flutter/PaystackFlutterPlugin.kt | 1 + lib/paystack.dart | 16 ++-------------- lib/{ => src}/models.dart | 0 lib/src/paystack_flutter.dart | 13 +++++++++++++ .../paystack_flutter_method_channel.dart | 0 .../paystack_flutter_platform_interface.dart | 0 test/paystack_flutter_method_channel_test.dart | 2 +- test/paystack_test.dart | 4 ++-- 8 files changed, 19 insertions(+), 17 deletions(-) rename lib/{ => src}/models.dart (100%) create mode 100644 lib/src/paystack_flutter.dart rename lib/{ => src}/paystack_flutter_method_channel.dart (100%) rename lib/{ => src}/paystack_flutter_platform_interface.dart (100%) 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 d08043e..e929435 100644 --- a/android/src/main/kotlin/com/paystack/paystack_flutter/PaystackFlutterPlugin.kt +++ b/android/src/main/kotlin/com/paystack/paystack_flutter/PaystackFlutterPlugin.kt @@ -26,6 +26,7 @@ class PaystackFlutterPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { channel.setMethodCallHandler(this) } + // TODO: Change error codes to numbers and provide documentation for it override fun onMethodCall(call: MethodCall, result: Result) { when (call.method) { "initialize" -> { diff --git a/lib/paystack.dart b/lib/paystack.dart index 1ce5fc4..248de7b 100644 --- a/lib/paystack.dart +++ b/lib/paystack.dart @@ -1,14 +1,2 @@ -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); - } -} +library Paystack; +export "src/paystack_flutter.dart" show Paystack; \ No newline at end of file diff --git a/lib/models.dart b/lib/src/models.dart similarity index 100% rename from lib/models.dart rename to lib/src/models.dart 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/paystack_flutter_method_channel.dart b/lib/src/paystack_flutter_method_channel.dart similarity index 100% rename from lib/paystack_flutter_method_channel.dart rename to lib/src/paystack_flutter_method_channel.dart diff --git a/lib/paystack_flutter_platform_interface.dart b/lib/src/paystack_flutter_platform_interface.dart similarity index 100% rename from lib/paystack_flutter_platform_interface.dart rename to lib/src/paystack_flutter_platform_interface.dart diff --git a/test/paystack_flutter_method_channel_test.dart b/test/paystack_flutter_method_channel_test.dart index 1b78a4e..1e41ea6 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(); diff --git a/test/paystack_test.dart b/test/paystack_test.dart index b0d3be6..c6c0805 100644 --- a/test/paystack_test.dart +++ b/test/paystack_test.dart @@ -1,8 +1,8 @@ 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/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 From e44960975212ca7f8b2544075786361b1a9a8339 Mon Sep 17 00:00:00 2001 From: Damilola Odujoko Date: Wed, 12 Feb 2025 12:51:41 +0100 Subject: [PATCH 4/4] Update interfaces to allow response return --- README.md | 65 ++++++++++++++++--- .../paystack_flutter/PaystackFlutterPlugin.kt | 23 ++++--- example/lib/main.dart | 3 +- .../PaystackFlutterPlugin.swift | 15 +++-- lib/src/paystack_flutter_method_channel.dart | 27 ++++---- .../paystack_flutter_method_channel_test.dart | 2 +- test/paystack_test.dart | 22 ++++--- 7 files changed, 106 insertions(+), 51 deletions(-) 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 e929435..f754aeb 100644 --- a/android/src/main/kotlin/com/paystack/paystack_flutter/PaystackFlutterPlugin.kt +++ b/android/src/main/kotlin/com/paystack/paystack_flutter/PaystackFlutterPlugin.kt @@ -19,7 +19,7 @@ class PaystackFlutterPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { private lateinit var channel : MethodChannel private lateinit var paymentSheet: PaymentSheet private var activity: FlutterFragmentActivity? = null - private var pendingResult: Result? = null + private lateinit var pendingResult: Result override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { channel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.paystack.flutter") @@ -28,6 +28,7 @@ class PaystackFlutterPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { // 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") @@ -57,53 +58,55 @@ class PaystackFlutterPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { .setPublicKey(publicKey) .setLoggingEnabled(enableLogging) .build() - pendingResult?.success(true) + pendingResult.success(true) } catch (e: Exception) { - pendingResult?.error("INITIALIZATION_ERROR", e.message, null) + pendingResult.error("INITIALIZATION_ERROR", e.message, null) } + +// pendingResult = null } private fun launch(accessCode: String) { if (activity == null) { - pendingResult?.error("NO_ACTIVITY", "Activity is not available", 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) + pendingResult.error("LAUNCH_ERROR", e.message, null) return } - pendingResult = null +// pendingResult = null } private fun paymentComplete(paymentSheetResult: PaymentSheetResult) { when (paymentSheetResult) { is PaymentSheetResult.Completed -> { - pendingResult?.success(mapOf( + pendingResult.success(mapOf( "status" to true, "message" to "Transaction successful", "reference" to paymentSheetResult.paymentCompletionDetails.reference )) } is PaymentSheetResult.Cancelled -> { - pendingResult?.success(mapOf( + pendingResult.success(mapOf( "status" to false, "message" to "Transaction cancelled", "reference" to "" )) } is PaymentSheetResult.Failed -> { - pendingResult?.success(mapOf( + pendingResult.success(mapOf( "status" to "failed", "message" to paymentSheetResult.error.message, "reference" to "" )) } } - pendingResult = null +// pendingResult = null } override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { diff --git a/example/lib/main.dart b/example/lib/main.dart index 477679b..4283c02 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'dart:async'; +import 'dart:developer'; import 'package:flutter/services.dart'; import 'package:paystack_flutter/paystack.dart'; @@ -30,7 +31,7 @@ class _MyAppState extends State { Future initialize(String publicKey) async { try { final response = await _paystack.initialize(publicKey, true); - print(response); + log(response as String); } on PlatformException catch (e) { print(e); } diff --git a/ios/paystack_flutter/Sources/paystack_flutter/PaystackFlutterPlugin.swift b/ios/paystack_flutter/Sources/paystack_flutter/PaystackFlutterPlugin.swift index ac470e7..0452922 100644 --- a/ios/paystack_flutter/Sources/paystack_flutter/PaystackFlutterPlugin.swift +++ b/ios/paystack_flutter/Sources/paystack_flutter/PaystackFlutterPlugin.swift @@ -14,6 +14,7 @@ public class PaystackFlutterPlugin: NSObject, FlutterPlugin { } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + self.result = result switch call.method { case "initialize": guard let args = call.arguments as? [String: Any], @@ -50,9 +51,9 @@ public class PaystackFlutterPlugin: NSObject, FlutterPlugin { } paystack = try instance.build() - result?(true) + self.result?(true) } catch { - result?(FlutterError(code: "INITIALIZATION_ERROR", + self.result?(FlutterError(code: "INITIALIZATION_ERROR", message: error.localizedDescription, details: nil)) } @@ -60,21 +61,21 @@ public class PaystackFlutterPlugin: NSObject, FlutterPlugin { private func launch(accessCode: String) { guard let paystack = paystack else { - result?(FlutterError(code: "NOT_INITIALIZED", + self.result?(FlutterError(code: "NOT_INITIALIZED", message: "Paystack not initialized", details: nil)) return } guard #available(iOS 14.0, *) else { - result?(FlutterError(code: "UNSUPPORTED_VERSION", + 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 { - result?(FlutterError(code: "NO_VIEW_CONTROLLER", + self.result?(FlutterError(code: "NO_VIEW_CONTROLLER", message: "Unable to present payment UI", details: nil)) return @@ -88,8 +89,8 @@ public class PaystackFlutterPlugin: NSObject, FlutterPlugin { }) } - private func paymentDone(_ result: TransactionResult) { - switch (result) { + private func paymentDone(_ transactionResult: TransactionResult) { + switch (transactionResult) { case .completed(let details): self.result?([ "status": true, diff --git a/lib/src/paystack_flutter_method_channel.dart b/lib/src/paystack_flutter_method_channel.dart index 599802b..bc2b3dd 100644 --- a/lib/src/paystack_flutter_method_channel.dart +++ b/lib/src/paystack_flutter_method_channel.dart @@ -11,35 +11,30 @@ class MethodChannelPaystackFlutter extends PaystackFlutterPlatform { @override Future initialize(String publicKey, bool enableLogging) async { try { - final response = await methodChannel.invokeMethod('initialize', { - 'publicKey': publicKey, - 'enableLogging': enableLogging - }); + final response = await methodChannel.invokeMethod('initialize', + {'publicKey': publicKey, 'enableLogging': enableLogging}); - return response as bool; + return response!; } on PlatformException catch (e) { throw PaystackException( - message: e.message ?? 'Failed to initialize Paystack', - code: e.code, - details: e.details - ); + 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 - }); + 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 - ); + message: e.message ?? 'Transaction Failed', + code: e.code, + details: e.details); } } } diff --git a/test/paystack_flutter_method_channel_test.dart b/test/paystack_flutter_method_channel_test.dart index 1e41ea6..1c328f1 100644 --- a/test/paystack_flutter_method_channel_test.dart +++ b/test/paystack_flutter_method_channel_test.dart @@ -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 c6c0805..b632f30 100644 --- a/test/paystack_test.dart +++ b/test/paystack_test.dart @@ -1,6 +1,5 @@ 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/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'; @@ -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"); }); }