In Flutter, the Android and iOS platforms are generated only once.
There are two ways to integrate third party SDKs and Native components:
A simple approach: Update the generated native projects directly.
A more modular and universal approach: Create a standalone module(plugin), which is then linked and added to the Flutter application.
For simplicity, this guide will focus only on the first option.
Integrating D1 SDK to a Flutter application
This guide was tested with:
D1 SDK
4.0.0 Android
4.0.0 iOS
Flutter 3.16.9
Android Studio Giraffe | 2022.3.1
Xcode 14.3.1
To create a new Flutter application:
To be able to configure the D1 SDK on the Android platform for the Push provisioning use case, the application ID needs to be registered for Google Pay. Use the application ID com.thalesgroup.gemalto.d1.validation for the sandbox testing environment.
Android platform
1
Adding D1 SDK dependencies to the Flutter application.
Add 'tools:replace="android:label"' to <application> element and xmlns:tools="http://schemas.android.com/tools" to <manifest> element at AndroidManifest.xml.
To run the NFC Wallet use case on the Android platform, the application needs to be signed with a specific keystore.
7
Add Firebase Cloud Messaging (FCM).
As NFC Wallet on the Android platform uses Firebase Cloud Messaging (FCM), Google Messaging Services has to be enabled in the Android application and the google-services.json file needs to be added.
Add the google-services.json to the Android project: validation/android/app/google-services.json.
Run the following command to generate the initial Podfile.
3
Update the iOS application Podfile.
4
Implement the Native iOS entry point of the plugin.
The following code snippets show how to create the method prototypes for the Login and Configure use cases.
Calling the D1 Plugin API from the Flutter application
After the native parts for the Android and iOS platform have been added to the Flutter project, the D1 API can be accessed from the Flutter application.
include ':d1plugin-libs-debug'
project(':d1plugin-libs-debug').projectDir = new File('/path/to/d1plugin-libs-debug/android')
include ':d1plugin-libs-release'
project(':d1plugin-libs-release').projectDir = new File('/path/to/sdk/d1plugin-libs-release/android')
target 'Runner' do
use_frameworks!
use_modular_headers!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
pod 'd1-libs-debug', :path => '../../sdk/d1plugin-libs-debug/ios/', :configurations => ['Debug']
pod 'd1-libs-release', :path => '../../sdk/d1plugin-libs-release/ios/', :configurations => ['Release']
end
validation/ios/Runner/AppDelegate.swift
import UIKit
import Flutter
import D1
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
private var d1Task: D1Task?
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let d1PluginChannel = FlutterMethodChannel(name: "d1plugin",
binaryMessenger: controller.binaryMessenger)
d1PluginChannel.setMethodCallHandler({
(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
switch call.method {
case "configure":
self.configure(call, result: result)
break
case "login":
self.login(call, result: result)
break
case "logout":
self.logout(result: result)
break
default:
result(FlutterMethodNotImplemented)
}
})
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
/// Configures D1 SDK.
/// - Parameters:
/// - call: Call.
/// - result: Result.
private func configure(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
let args = call.arguments as! Dictionary<String, Any>
let consumerId = args["consumerId"] as! String
let d1ServiceUrl = args["d1ServiceUrl"] as! String
let digitalCardUrl = args["consumerId"] as! String
let issuerId = args["issuerId"] as! String
let modulus = args["modulus"] as! String
let exponent = args["exponent"] as! String
var comp = D1Task.Components()
comp.d1ServiceURLString = d1ServiceUrl
comp.issuerID = issuerId
comp.d1ServiceRSAExponent = dataWithHexString(hex: exponent)
comp.d1ServiceRSAModulus = dataWithHexString(hex: modulus)
comp.digitalCardURLString = digitalCardUrl
d1Task = comp.task()
let coreConfig = ConfigParams.coreConfig(consumerID: consumerId)
// required for Card Processing & OEM Pay
let cardConfig = ConfigParams.cardConfig()
getD1Task().configure([coreConfig, cardConfig]) { errors in
if let errors = errors {
result(FlutterError(code: String(errors[0].code.rawValue),
message: errors[0].localizedDescription,
details: nil))
} else {
result(nil)
}
}
}
/// Logs in to D1 services.
/// - Parameters:
/// - call: Call.
/// - result: Result.
private func login(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
let args = call.arguments as! Dictionary<String, Any>
let authToken = args["authToken"] as! String
if var issueTokenData: Data = authToken.data(using: .utf8) {
getD1Task().login(&issueTokenData) { error in
if let error = error {
result(FlutterError(code: String(error.code.rawValue),
message: error.localizedDescription,
details: nil))
} else {
result(nil)
}
}
}
}
/// Logs out from D1 services.
/// - Parameters:
/// - call: Call.
/// - result: Result.
private func logout(result: @escaping FlutterResult) {
getD1Task().logout() { error in
if let error = error {
result(FlutterError(code: String(error.code.rawValue),
message: error.localizedDescription,
details: nil))
} else {
result(nil)
}
}
}
/// Converts hexa string to Data.
/// - Parameter hex: Input hexa string.
/// - Returns: Hexa string converted to Data.
private func dataWithHexString(hex: String) -> Data {
var hex = hex
var data = Data()
while(hex.count > 0) {
let subIndex = hex.index(hex.startIndex, offsetBy: 2)
let c = String(hex[..<subIndex])
hex = String(hex[subIndex...])
var ch: UInt64 = 0
Scanner(string: c).scanHexInt64(&ch)
var char = UInt8(ch)
data.append(&char, count: 1)
}
return data
}
/// Retrieves the D1Task. Need to configure the D1 SDK first.
/// - Returns: D1Task object. fatalError is thrown if called without D1 SDK configuration.
private func getD1Task() -> D1Task {
if let d1Task = d1Task {
return d1Task
} else {
fatalError("Need to configure D1 SDK first.")
}
}
}
final methodChannel = const MethodChannel('d1plugin');
// Configure
try {
var arguments = <String, String>{
'consumerId': CONSUMER_ID,
'd1ServiceUrl': D1_SERVICE_URL,
'digitalCardUrl': DIGITAL_CARD_URL,
'issuerId': ISSUER_ID,
'modulus': D1_SERVICE_RSA_MODULUS,
'exponent': D1_SERVICE_RSA_EXPONENT,
};
await methodChannel.invokeMethod<void>('configure', arguments);
// Configure OK.
} on PlatformException catch (e) {
// Handle error.
}
// Login
try {
var arguments = <String, String>{'authToken': "<auth-token>"};
await methodChannel.invokeMethod<void>('login', arguments);
// Login OK.
} on PlatformException catch (e) {
// Handle error.
}
// Logout
try {
await methodChannel.invokeMethod<void>('logout');
// Logout OK.
} on PlatformException catch (e) {
// Handle error.
}