diff --git a/androidApp/src/main/java/com/cards/session/android/CustomTextField.kt b/androidApp/src/main/java/com/cards/session/android/CustomTextField.kt new file mode 100644 index 0000000..96ea9b7 --- /dev/null +++ b/androidApp/src/main/java/com/cards/session/android/CustomTextField.kt @@ -0,0 +1,40 @@ +package com.cards.session.android + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun CustomTextField( + label: String, + placeholder: String = "", + onValueChange: (String) -> Unit +) { + var text by remember { mutableStateOf("") } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + OutlinedTextField( + value = text, + onValueChange = { newValue -> + text = newValue + onValueChange(newValue) + }, + label = { Text(text = label) }, + placeholder = { Text(text = placeholder) }, + modifier = Modifier.fillMaxWidth() + ) + } +} \ No newline at end of file diff --git a/androidApp/src/main/java/com/cards/session/android/MainActivity.kt b/androidApp/src/main/java/com/cards/session/android/MainActivity.kt index 24edb06..f13b00b 100644 --- a/androidApp/src/main/java/com/cards/session/android/MainActivity.kt +++ b/androidApp/src/main/java/com/cards/session/android/MainActivity.kt @@ -60,6 +60,7 @@ fun AppRoot() { } val state by cardSessions.state.collectAsState() val scope = CoroutineScope(Dispatchers.Main) + var paymentSessionId = "" NavHost( navController = navController, @@ -78,6 +79,19 @@ fun AppRoot() { textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth() ) + + Spacer(modifier = Modifier.height(16.dp)) + + CustomTextField( + label = "Enter paymentSessionId", + placeholder = "paymentSessionId", + onValueChange = { newText -> + // Handle text change here + paymentSessionId = newText + println("New text: $newText") + } + ) + Spacer(modifier = Modifier.height(16.dp)) Button( content = { Text(text = "Collect Card Data") }, @@ -91,8 +105,8 @@ fun AppRoot() { cardholderFirstName = "First", cardholderLastName = "Name", cardholderEmail = "firstname@xendit.co", - cardholderPhoneNumber = "01231245242", - paymentSessionId = "ps-1234567890abcdef12345678" + cardholderPhoneNumber = "+123456789", + paymentSessionId = paymentSessionId ) } } @@ -104,7 +118,7 @@ fun AppRoot() { scope.launch { cardSessions.collectCvn( cvn = "123", - paymentSessionId = "ps-1234567890abcdef12345678" + paymentSessionId = paymentSessionId ) } } diff --git a/cardsSdk/build.gradle.kts b/cardsSdk/build.gradle.kts index 8d97ca6..85cd97b 100644 --- a/cardsSdk/build.gradle.kts +++ b/cardsSdk/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework + plugins { alias(libs.plugins.kotlin.multiplatform) alias(libs.plugins.kotlin.native.cocoapods) @@ -10,16 +12,43 @@ kotlin { iosArm64() iosSimulatorArm64() + val xcframeworkName = "cardSdk" + val xcFrameworkVersion = "1.0.0" + val xcFrameworkBundleVersion = "1" // Increase it everytime version changed + val xcf = XCFramework(xcframeworkName) + + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64(), + ).forEach { + it.binaries.framework { + baseName = xcframeworkName + + // Specify CFBundleIdentifier to uniquely identify the framework + binaryOption("bundleId", "com.cards.session.${xcframeworkName}") + binaryOption("bundleShortVersionString", xcFrameworkVersion) + binaryOption("bundleVersion", xcFrameworkBundleVersion) + xcf.add(this) + isStatic = true + } + } + cocoapods { summary = "Cards Session SDK module" homepage = "Link to the Cards Session Module homepage" version = "1.0" - ios.deploymentTarget = "16.0" + ios.deploymentTarget = "14.0" podfile = project.file("../iosApp/Podfile") framework { baseName = "cardsSdk" isStatic = true } + + pod("XenditFingerprintSDK") { + version = "1.0.1" + extraOpts += listOf("-compiler-option", "-fmodules") + } } sourceSets { diff --git a/cardsSdk/cardsSdk.podspec b/cardsSdk/cardsSdk.podspec index 71e7abd..9050a33 100644 --- a/cardsSdk/cardsSdk.podspec +++ b/cardsSdk/cardsSdk.podspec @@ -8,8 +8,8 @@ Pod::Spec.new do |spec| spec.summary = 'Cards Session SDK module' spec.vendored_frameworks = 'build/cocoapods/framework/cardsSdk.framework' spec.libraries = 'c++' - spec.ios.deployment_target = '16.0' - + spec.ios.deployment_target = '14.0' + spec.dependency 'XenditFingerprintSDK', '1.0.1' if !Dir.exist?('build/cocoapods/framework/cardsSdk.framework') || Dir.empty?('build/cocoapods/framework/cardsSdk.framework') raise " diff --git a/cardsSdk/src/iosMain/kotlin/com/cards/session/cards/sdk/CardSessions.kt b/cardsSdk/src/iosMain/kotlin/com/cards/session/cards/sdk/CardSessions.kt index dd9270f..09884ed 100644 --- a/cardsSdk/src/iosMain/kotlin/com/cards/session/cards/sdk/CardSessions.kt +++ b/cardsSdk/src/iosMain/kotlin/com/cards/session/cards/sdk/CardSessions.kt @@ -2,4 +2,8 @@ package com.cards.session.cards.sdk actual fun CardSessions.Factory.create(apiKey: String): CardSessions { return CardSessionsImpl.create(apiKey) -} \ No newline at end of file +} + +fun CardSessions.Factory.create(apiKey: String, isEnableLog: Boolean): CardSessions { + return CardSessionsImpl.create(apiKey, isEnableLog) +} \ No newline at end of file diff --git a/cardsSdk/src/iosMain/kotlin/com/cards/session/cards/sdk/CardSessionsImpl.kt b/cardsSdk/src/iosMain/kotlin/com/cards/session/cards/sdk/CardSessionsImpl.kt index 7073c67..ffddf17 100644 --- a/cardsSdk/src/iosMain/kotlin/com/cards/session/cards/sdk/CardSessionsImpl.kt +++ b/cardsSdk/src/iosMain/kotlin/com/cards/session/cards/sdk/CardSessionsImpl.kt @@ -1,5 +1,7 @@ package com.cards.session.cards.sdk +import cocoapods.XenditFingerprintSDK.FingerprintSDK +import cocoapods.XenditFingerprintSDK.LogModeAll import com.cards.session.cards.models.CardsRequestDto import com.cards.session.cards.models.CardsResponseDto import com.cards.session.cards.models.DeviceFingerprint @@ -12,15 +14,18 @@ import com.cards.session.util.AuthTokenGenerator import com.cards.session.util.Logger import com.cardsession.sdk.CreditCardUtil import io.ktor.client.HttpClient +import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import platform.Foundation.NSLog -internal class CardSessionsImpl private constructor( +internal class CardSessionsImpl @OptIn(ExperimentalForeignApi::class) +private constructor( private val apiKey: String, - private val httpClient: HttpClient + private val httpClient: HttpClient, + private val fingerprintSDK: FingerprintSDK ) : CardSessions { private val TAG = "CardSessionsImpl" private val logger = Logger(TAG) @@ -129,17 +134,40 @@ internal class CardSessionsImpl private constructor( } } + @OptIn(ExperimentalForeignApi::class) private fun getFingerprint(eventName: String): String { - // TODO: Implement iOS-specific device fingerprinting - return "" + fingerprintSDK.scanWithEvent_name( + event_name = eventName, + event_id = eventName, + completion = { response, errorMsg -> + if (errorMsg != null) { + logger.e("Scan failed for event: $eventName, error: $errorMsg") + } + } + ) + + return fingerprintSDK.getSessionId() } companion object { - fun create(apiKey: String): CardSessions { - Logger("").debugBuild() + @OptIn(ExperimentalForeignApi::class) + fun create(apiKey: String, isEnableLog: Boolean = false): CardSessions { + val fingerprint = FingerprintSDK() + + if (isEnableLog) { + Logger("").debugBuild() + } + + try { + fingerprint.initSDKWithApiKey(apiKey) + } catch (e: Exception) { +// Logger("CardSessions").e( "Failed to initialize XenditFingerprintSDK", e) + } + return CardSessionsImpl( apiKey = apiKey, - httpClient = HttpClientFactory().create() + httpClient = HttpClientFactory().create(), + fingerprintSDK = fingerprint ) } } diff --git a/cortex.yaml b/cortex.yaml index 0a3b70c..1b526ae 100644 --- a/cortex.yaml +++ b/cortex.yaml @@ -1,11 +1,11 @@ openapi: 3.0.1 info: - title: xendit-cards-sdk + title: cards-session-mobile-sdk description: x-cortex-git: github: - repository: xendit/xendit-cards-sdk - x-cortex-tag: xendit-cards-sdk + repository: xendit/cards-session-mobile-sdk + x-cortex-tag: cards-session-mobile-sdk x-cortex-dependency: [] x-cortex-service-groups: - P2 diff --git a/iosApp/Podfile.lock b/iosApp/Podfile.lock index 37e9dcf..9f30637 100644 --- a/iosApp/Podfile.lock +++ b/iosApp/Podfile.lock @@ -1,15 +1,22 @@ PODS: - - cardsSdk (1.0) + - cardsSdk (1.0): + - XenditFingerprintSDK (= 1.0.1) + - XenditFingerprintSDK (1.0.1) DEPENDENCIES: - cardsSdk (from `../cardsSdk`) +SPEC REPOS: + trunk: + - XenditFingerprintSDK + EXTERNAL SOURCES: cardsSdk: :path: "../cardsSdk" SPEC CHECKSUMS: - cardsSdk: 7f311fe8287c75352cb28bd092ef18f02b56f338 + cardsSdk: 9e0b4e91658434b731daf4d2262393018fe4f742 + XenditFingerprintSDK: 29410a2dcf467fe33d6e0167f590deae47c753ee PODFILE CHECKSUM: 2d3ff3b1724835dab0bcce0b112c67a9f1440453 diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index fbb55b7..2358379 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -12,6 +12,7 @@ 0C0CDBC02D06AD03002DC16A /* CardSessionsImpl.kt in Resources */ = {isa = PBXBuildFile; fileRef = 0C0CDBBE2D06AD03002DC16A /* CardSessionsImpl.kt */; }; 0C0CDBC62D06DB5C002DC16A /* CardSessions.kt in Resources */ = {isa = PBXBuildFile; fileRef = 0C0CDBC52D06DB5C002DC16A /* CardSessions.kt */; }; 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; }; + 21FA6F202D0997E600DD5E13 /* CustomTextFieldStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21FA6F1F2D0997E600DD5E13 /* CustomTextFieldStyle.swift */; }; 2F9A03F732FC062E0B94E281 /* Pods_iosApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 83E47D7C75F016D7EF416763 /* Pods_iosApp.framework */; }; 7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; }; /* End PBXBuildFile section */ @@ -22,6 +23,7 @@ 0C0CDBBE2D06AD03002DC16A /* CardSessionsImpl.kt */ = {isa = PBXFileReference; lastKnownFileType = text; name = CardSessionsImpl.kt; path = ../cardsSdk/src/iosMain/kotlin/com/cards/session/cards/sdk/CardSessionsImpl.kt; sourceTree = SOURCE_ROOT; }; 0C0CDBC52D06DB5C002DC16A /* CardSessions.kt */ = {isa = PBXFileReference; lastKnownFileType = text; name = CardSessions.kt; path = ../cardsSdk/src/commonMain/kotlin/com/cards/session/cards/sdk/CardSessions.kt; sourceTree = SOURCE_ROOT; }; 2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; }; + 21FA6F1F2D0997E600DD5E13 /* CustomTextFieldStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextFieldStyle.swift; sourceTree = ""; }; 42AF08E65F4330F0220806A5 /* Pods-iosApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.debug.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.debug.xcconfig"; sourceTree = ""; }; 7555FF7B242A565900829871 /* iosApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iosApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -75,6 +77,7 @@ 0C0CDBBE2D06AD03002DC16A /* CardSessionsImpl.kt */, 058557BA273AAA24004C7B11 /* Assets.xcassets */, 7555FF82242A565900829871 /* ContentView.swift */, + 21FA6F1F2D0997E600DD5E13 /* CustomTextFieldStyle.swift */, 7555FF8C242A565B00829871 /* Info.plist */, 2152FB032600AC8F00CF470E /* iOSApp.swift */, 058557D7273AAEEB004C7B11 /* Preview Content */, @@ -110,6 +113,7 @@ 7555FF77242A565900829871 /* Sources */, 7555FF79242A565900829871 /* Resources */, 50F90884C22C0FEF776BD342 /* Frameworks */, + 0321158BB61A2C062DF08FCD /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -168,6 +172,23 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 0321158BB61A2C062DF08FCD /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 87FCF521B92BEDD427DAB3FE /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -199,6 +220,7 @@ files = ( 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */, 7555FF83242A565900829871 /* ContentView.swift in Sources */, + 21FA6F202D0997E600DD5E13 /* CustomTextFieldStyle.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/iosApp/iosApp/ContentView.swift b/iosApp/iosApp/ContentView.swift index 4f5aa5f..4e63672 100644 --- a/iosApp/iosApp/ContentView.swift +++ b/iosApp/iosApp/ContentView.swift @@ -7,11 +7,13 @@ struct ContentView: View { @State private var isLoading = false @State private var error: String? @State private var cardResponse: String? + @State private var paymentSessionId = "" init(appModule: AppModule) { self.appModule = appModule self.cardSessions = CardSessionsFactory().create( - apiKey: "API_KEY_HERE" + apiKey: "API_KEY_HERE", + isEnableLog: true ) } @@ -23,6 +25,13 @@ struct ContentView: View { .multilineTextAlignment(.center) .frame(maxWidth: .infinity) + VStack(spacing: 8) { + Text("paymentSessionId: ") + TextField("Key in payment session id", text: $paymentSessionId) + .textFieldStyle(CustomTextFieldStyle()) + .keyboardType(.default) + } + Button(action: { Task { isLoading = true @@ -36,8 +45,8 @@ struct ContentView: View { cardholderFirstName: "First", cardholderLastName: "Name", cardholderEmail: "firstname@xendit.co", - cardholderPhoneNumber: "01231245242", - paymentSessionId: "ps-1234567890abcdef12345678" + cardholderPhoneNumber: "+123456789", + paymentSessionId: paymentSessionId ) cardResponse = response.description } catch { @@ -56,7 +65,7 @@ struct ContentView: View { do { let response = try await cardSessions.collectCvn( cvn: "123", - paymentSessionId: "ps-1234567890abcdef12345678" + paymentSessionId: paymentSessionId ) cardResponse = response.description } catch { @@ -82,6 +91,3 @@ struct ContentView: View { .padding() } } - - - diff --git a/iosApp/iosApp/CustomTextFieldStyle.swift b/iosApp/iosApp/CustomTextFieldStyle.swift new file mode 100644 index 0000000..f40996a --- /dev/null +++ b/iosApp/iosApp/CustomTextFieldStyle.swift @@ -0,0 +1,22 @@ +// +// CustomTextFieldStyle.swift +// Pods +// +// Created by Ahmad Alfhajri on 11/12/2024. +// + +import SwiftUI + +struct CustomTextFieldStyle: TextFieldStyle { + func _body(configuration: TextField) -> some View { + configuration + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background( + RoundedRectangle(cornerRadius: 8) + .stroke(Color.gray.opacity(0.3), lineWidth: 1) + .background(Color.white) + ) + .font(.system(size: 16)) + } +}