As default customization, you can start your flutter app running flutter create app_name
As you have your flutter app already created, you need to setup the iOS internal environment here:
I recommend you to open the iOS folder using XCode IDE.
Once you open the iOS folder, you will have to import PomeloCardsSDK
library using Swift Package Manager, Select you project, go to Package Dependencies and add git@github.com:pomelo-la/cards-ios.git
- Setup minimum deployment target to iOS 13.0 or later
- Setup NSFaceIDUsageDescription on info.plist with the appropriate message.
- Ex:
$(PRODUCT_NAME) uses Face ID to validate your identity
Method channel is the tool we chose to setup a safe connection between flutter app and iOS native methods. To achieve this, you will need to configure both sides, flutter and iOS:
Choose a channel ID (String variable) and create the channel with that identifier; in the example below, the id chosen is "com.example.app/message".
class _MyAppState extends State<MyApp> {
static const platform = const MethodChannel('com.example.app/message');
(...)
}
Once the channel is defined, we call it ‘platform’, we are able to send and receive data between both sides.
To receive data, you have to implement the method setMethodCallHandler, in the example, we configured it on the init method:
@override
void initState() {
super.initState();
platform.setMethodCallHandler((MethodCall call) async {
(...)
});
}
Inside this implementation, flutter will receive the methods called by the iOS side.
As you need to fetch iOS methods, you can access to those ones by using the ‘invokeMethod’ function with the string name of the iOS method you need to invoke, check the example below as we need to execute the ‘launchCardViewWidget’ iOS method :
await platform.invokeMethod('launchCardViewWidget');
Here you have another example executing sendMessageToiOS
method with an argument’:
await platform.invokeMethod('sendMessageToiOS', {"message": message});
A complete method to define those invocation methods would be this one:
void _sendMessageToiOS(String message) async {
String responseMessage = "";
try {
responseMessage =
await platform.invokeMethod('sendMessageToiOS', {"message": message});
} on PlatformException catch (e) {
print("Failed to send message: '${e.message}'.");
}
setState(() {
_responseMessage = responseMessage;
});
}
To complete the method channel configuration, we need to setup the AppDelegate, which is the main entry point of an iOS app:
As a few steps before you added the packages, now you need to import manually those dependencies to the appDelegate file, at the top of it:
import UIKit
import Flutter
import PomeloNetworking
import PomeloUI
import PomeloCards
AppDelegate class, inherits from UIApplicationDelegate by default, but, we need to make this class inherits from FlutterAppDelegate:
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
(...)
}
Moreover, we need to mark with override keyword the method didFinishLaunchingWithOptions:
override func application( _ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool {
(...)
}
With this new inheritance from FlutterAppDelegate, the appDelegate loses some native properties, so it is needed to provide them. One of them, is the UIWindow reference, this object describe the main frame that allows us to present the rootViewController, so, inside the
didFinishLaunchWithOptionsMethod, copy this code:
let controller : FlutterViewController = HomeViewController()
self.window = UIWindow(frame: UIScreen.main.bounds)
navigationController = UINavigationController(rootViewController: controller)
window?.rootViewController = navigationController
window.makeKeyAndVisible()
This way, we are adding a custom viewController ( HomeViewController()
) as our first controller of the iOS app, in addition, this controller is a FlutterViewController object type that handle the execution of the Flutter engine within an iOS view. This means that it can be used to display a Flutter UI within a native iOS app screen, allowing for tighter integration between the native part of the app and the Flutter part.
Now that we have our FlutterViewController
as the root one, we need to remove the default root view instance from the project, it is the storyboard and their references, move to trash de Main.storyboard file:
And remove from the Info.plist file, the row Main storyboard file base name
After that, we will setup the methodChannel property as an optional type into the AppDelegate class
var messageChannel: FlutterMethodChannel?
Finally, as you have your FlutterViewController
object and your methodChannel
property, now you are able to finish the connection,
providing a new FlutterMethodChannel
object with the binaryMessenger
object and calling the setMethodCallHandler
, needed to receive the calls from Flutter side:
messageChannel = FlutterMethodChannel(name: "com.example.app/message", binaryMessenger: controller.binaryMessenger)
messageChannel?.setMethodCallHandler({ [weak self] (call: FlutterMethodCall,
result: @escaping FlutterResult) -> Void in
(...)
})
The BinaryMessenger class provides methods for sending and receiving messages of two types: basic messages and platform messages.
As flutter side, the method to call external methos is ‘invokeMethod’ with the ID (string) of these one, check the example below:
messageChannel?.invokeMethod("receivedMessageFromiOSSide", arguments: [message])
To initialize Pomelo Cards SDK, we need to provide an end user token. All the logic is implemented in swift on the iOS side, you can check how to do that here: https://github.com/pomelo-la/cards-ios/tree/feature/documentation#3-authorization You need to setup PomeloNetworking environment to get and decode the valid token, so create a new class to delegate the functionality:
//
// ClientAuthorizationService.swift
// Runner
//
import Foundation
import PomeloNetworking
class ClientAuthorizationService: PomeloAuthorizationServiceProtocol {
var clientToken: String?
func getValidToken(completionHandler: @escaping (String?) -> Void) {
let session = URLSession.shared
guard let urlRequest = buildRequest(email: Constants.email) else {
print("\(String.userTokenError) cannot build request")
completionHandler(nil)
return
}
let task = session.dataTask(with: urlRequest) { data, response, error in
if let error = error {
print("\(String.userTokenError) \(error)")
completionHandler(nil)
return
}
let decoder = JSONDecoder()
do {
let dto = try decoder.decode(PomeloAccessTokenDTO.self, from: data!)
completionHandler(dto.accessToken)
} catch {
print("\(String.userTokenError) \(error.localizedDescription)")
completionHandler(nil)
}
}
task.resume()
}
private func buildRequest(email: String) -> URLRequest? {
let url = URL(string: Constants.endPoint)
var urlRequest = URLRequest(url: url!)
urlRequest.httpMethod = "POST"
let body = [
String.BodyParams.email: "\(email)"
]
do {
let jsonEncoder = JSONEncoder()
jsonEncoder.outputFormatting = .withoutEscapingSlashes
urlRequest.httpBody = try jsonEncoder.encode(body)
} catch {
print(error.localizedDescription)
return nil
}
urlRequest.setValue("application/json", forHTTPHeaderField: String.AuthHeaders.contentType)
return urlRequest
}
}
fileprivate extension String {
static let userTokenError = "Cards Sample App error!🔥 - EndUserTokenService: "
struct AuthHeaders {
static let contentType = "Content-Type"
}
struct BodyParams {
static let userId = "user_id"
static let email = "email"
}
}
In addition, it is needed a test user to try, so create a struct with the full data:
struct Constants {
static let email = "juan.perez@pomelo.la"
static let cardholderName = "Juan Perez"
static let cardId = "crd-2LQY6Jrh6ScnBaJT7JHcX36ecQG"
static let lastFourCardDigits = "8016"
static let image = "TarjetaVirtual"
static let endPoint = "https://api-stage.pomelo.la/cards-sdk-be-sample/token"
}
In the AppDelegate
class, you can create the variable to use the client:
let client = ClientAuthorizationService()
Now you are able to initialize PomeloCards
:
PomeloNetworkConfigurator.shared.configure(authorizationService: client)
PomeloUIGateway.shared.initialize()
PomeloCards.initialize(with: PomeloCardsConfiguration(environment: .staging))
To customize the iOS theme you should setup your own theme as explained here: https://github.com/pomelo-la/cards-ios/tree/feature/documentation#customizing
An example of how you can insert PomeloCardViewController on your flutter app
private func getCard() -> UIViewController {
let widgetView = PomeloCardWidgetView(cardholderName: Constants.cardholderName,
lastFourCardDigits: Constants.lastFourCardDigits,
imageFetcher: PomeloImageFetcher(image: UIImage(named: Constants.image)!))
return (CardViewController(cardWidgetView: widgetView, cardId: Constants.cardId))
}
private func getCardList() -> UIViewController {
let widgetDetailViewController = PomeloCardWidgetDetailViewController()
widgetDetailViewController.showSensitiveData(cardId: Constants.cardId,
onPanCopy: {
print("Pan was coppied")
}, completionHandler: { result in
switch result {
case .success(): break
case .failure(let error):
print("Sensitive data error: \(error)")
}
})
return widgetDetailViewController
}
private func getActivationCardWidget() -> UIViewController {
let widgetCardActivationViewController = PomeloWidgetCardActivationViewController(completionHandler: { result in
switch result {
case .success(let cardId):
print("Card was activated. Card id: \(String(describing: cardId))")
case .failure(let error):
print("error")
}
})
return widgetCardActivationViewController
}
private func getPinWidget() -> UIViewController {
let widgetChangePinViewController = PomeloWidgetChangePinViewController(cardId: Constants.cardId,
completionHandler: { result in
switch result {
case .success(): break
case .failure(let error):
print("Change pin error: \(error)")
}
})
return widgetChangePinViewController
}
The final flutter screen of this project should look like this:
A few resources to get you started if this is your first Flutter project:
For help getting started with Flutter development, view the online documentation, which offers tutorials, samples, guidance on mobile development, and a full API reference.