Skip to content

Commit a655fc9

Browse files
Merge pull request #6 from xendit/finalizing-ios
Finalizing ios
2 parents 3183e1d + 521d2cc commit a655fc9

File tree

10 files changed

+312
-66
lines changed

10 files changed

+312
-66
lines changed

README.md

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Xendit Cards SDK
22

3-
A lightweight SDK for integrating card payments into Android applications. This SDK provides secure card data collection functionality with built-in validation and 3DS support.
3+
A lightweight SDK for integrating card payments into Android and iOS applications. This SDK provides secure card data collection functionality with built-in validation and 3DS support.
44

55
## Features
66

@@ -12,7 +12,7 @@ A lightweight SDK for integrating card payments into Android applications. This
1212

1313
## Installation
1414

15-
### Gradle
15+
### Android - Gradle
1616

1717
Add the following to your app's `build.gradle.kts`:
1818

@@ -26,17 +26,24 @@ dependencies {
2626

2727
### Initialize the SDK
2828

29-
First, initialize the CardSessions instance with your Xendit public key:
30-
29+
#### Android
3130
```kotlin
3231
val cardSessions = CardSessions.create(
3332
context = context,
3433
apiKey = "xnd_public_development_YOUR_KEY_HERE"
3534
)
3635
```
3736

37+
#### iOS
38+
```swift
39+
let cardSessions = CardSessionsFactory().create(
40+
apiKey: "xnd_public_development_YOUR_KEY_HERE"
41+
)
42+
```
43+
3844
### Collect Card Data
3945

46+
#### Android
4047
```kotlin
4148
// Collect complete card information
4249
val response = cardSessions.collectCardData(
@@ -52,21 +59,45 @@ val response = cardSessions.collectCardData(
5259
)
5360
```
5461

62+
#### iOS
63+
```swift
64+
// Using async/await
65+
let response = try await cardSessions.collectCardData(
66+
cardNumber: "4000000000001091",
67+
expiryMonth: "12",
68+
expiryYear: "2025",
69+
cvn: "123", // Optional
70+
cardholderFirstName: "John",
71+
cardholderLastName: "Doe",
72+
cardholderEmail: "[email protected]",
73+
cardholderPhoneNumber: "+1234567890",
74+
paymentSessionId: "ps-1234567890"
75+
)
76+
```
77+
5578
### Collect CVN Only
5679

5780
For saved cards where you only need to collect the CVN:
5881

82+
#### Android
5983
```kotlin
6084
val response = cardSessions.collectCvn(
6185
cvn = "123",
6286
paymentSessionId = "ps-1234567890"
6387
)
6488
```
6589

66-
### Monitor Session State
90+
#### iOS
91+
```swift
92+
let response = try await cardSessions.collectCvn(
93+
cvn: "123",
94+
paymentSessionId: "ps-1234567890"
95+
)
96+
```
6797

68-
The SDK provides a StateFlow to monitor the current state of operations:
98+
### Monitor Session State
6999

100+
#### Android
70101
```kotlin
71102
cardSessions.state.collect { state ->
72103
when {
@@ -77,6 +108,21 @@ cardSessions.state.collect { state ->
77108
}
78109
```
79110

111+
#### iOS
112+
```swift
113+
// Using Combine
114+
cardSessions.state
115+
.sink { state in
116+
if state.isLoading {
117+
// Show loading state
118+
} else if let error = state.error {
119+
// Handle error
120+
} else if let response = state.cardResponse {
121+
// Handle success
122+
}
123+
}
124+
```
125+
80126
## Response Types
81127

82128
### Success Response
@@ -107,4 +153,6 @@ The SDK automatically handles:
107153
## Requirements
108154

109155
- Android API level 21 or higher
156+
- iOS 13.0 or higher
157+
- Swift 5.5 or higher
110158

androidApp/src/main/java/com/cards/session/android/MainActivity.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ fun AppRoot() {
9292
cardholderLastName = "Name",
9393
cardholderEmail = "[email protected]",
9494
cardholderPhoneNumber = "01231245242",
95-
paymentSessionId = "session_id MUST be 27 chars"
95+
paymentSessionId = "ps-1234567890abcdef12345678"
9696
)
9797
}
9898
}
@@ -104,7 +104,7 @@ fun AppRoot() {
104104
scope.launch {
105105
cardSessions.collectCvn(
106106
cvn = "123",
107-
paymentSessionId = "1234567890"
107+
paymentSessionId = "ps-1234567890abcdef12345678"
108108
)
109109
}
110110
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.cards.session.util
2+
3+
import io.github.aakira.napier.Napier
4+
5+
actual class Logger actual constructor(private val tag: String) {
6+
actual fun d(message: String, throwable: Throwable?) {
7+
Napier.d(message, throwable, tag)
8+
}
9+
10+
actual fun i(message: String, throwable: Throwable?) {
11+
Napier.i(message, throwable, tag)
12+
}
13+
14+
actual fun w(message: String, throwable: Throwable?) {
15+
Napier.w(message, throwable, tag)
16+
}
17+
18+
actual fun e(message: String, throwable: Throwable?) {
19+
Napier.e(message, throwable, tag)
20+
}
21+
22+
actual fun debugBuild() {
23+
}
24+
}

cardsSdk/src/commonMain/kotlin/com/cards/session/util/Logger.kt

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,10 @@ package com.cards.session.util
22

33
import io.github.aakira.napier.Napier
44

5-
class Logger(private val tag: String) {
6-
fun d(message: String, throwable: Throwable? = null) {
7-
Napier.d(message, throwable, tag)
8-
}
9-
10-
fun i(message: String, throwable: Throwable? = null) {
11-
Napier.i(message, throwable, tag)
12-
}
13-
14-
fun w(message: String, throwable: Throwable? = null) {
15-
Napier.w(message, throwable, tag)
16-
}
17-
18-
fun e(message: String, throwable: Throwable? = null) {
19-
Napier.e(message, throwable, tag)
20-
}
5+
expect class Logger(tag: String) {
6+
fun d(message: String, throwable: Throwable? = null)
7+
fun i(message: String, throwable: Throwable? = null)
8+
fun w(message: String, throwable: Throwable? = null)
9+
fun e(message: String, throwable: Throwable? = null)
10+
fun debugBuild()
2111
}

cardsSdk/src/iosMain/kotlin/com/cards/session/cards/network/NetworkConstants.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import platform.Foundation.NSBundle
44

55
actual object NetworkConstants {
66
actual val BASE_URL: String
7-
get() = if (isDebugBuild()) "https://api.xendit.co/v3" else "https://api.stg.tidnex.dev/v3"
7+
get() = if (isDebugBuild()) "https://api.stg.tidnex.dev/v3" else "https://api.xendit.co/v3"
88

99
// please check this logic
1010
private fun isDebugBuild(): Boolean {
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package com.cards.session.cards.sdk
2+
3+
import com.cards.session.cards.models.CardsRequestDto
4+
import com.cards.session.cards.models.CardsResponseDto
5+
import com.cards.session.cards.models.DeviceFingerprint
6+
import com.cards.session.cards.network.CardsSessionError.UNKNOWN_ERROR
7+
import com.cards.session.cards.network.CardsSessionException
8+
import com.cards.session.cards.network.KtorCardsClient
9+
import com.cards.session.cards.ui.CardSessionState
10+
import com.cards.session.network.HttpClientFactory
11+
import com.cards.session.util.AuthTokenGenerator
12+
import com.cards.session.util.Logger
13+
import com.cardsession.sdk.CreditCardUtil
14+
import io.ktor.client.HttpClient
15+
import kotlinx.coroutines.flow.MutableStateFlow
16+
import kotlinx.coroutines.flow.StateFlow
17+
import kotlinx.coroutines.flow.asStateFlow
18+
import kotlinx.coroutines.flow.update
19+
import platform.Foundation.NSLog
20+
21+
internal class CardSessionsImpl private constructor(
22+
private val apiKey: String,
23+
private val httpClient: HttpClient
24+
) : CardSessions {
25+
private val TAG = "CardSessionsImpl"
26+
private val logger = Logger(TAG)
27+
private val client = KtorCardsClient(httpClient)
28+
private val _state = MutableStateFlow(CardSessionState())
29+
override val state: StateFlow<CardSessionState> = _state.asStateFlow()
30+
31+
override suspend fun collectCardData(
32+
cardNumber: String,
33+
expiryMonth: String,
34+
expiryYear: String,
35+
cvn: String?,
36+
cardholderFirstName: String,
37+
cardholderLastName: String,
38+
cardholderEmail: String,
39+
cardholderPhoneNumber: String,
40+
paymentSessionId: String
41+
): CardsResponseDto {
42+
_state.update { it.copy(isLoading = true, exception = null) }
43+
44+
try {
45+
// Validate card data
46+
when {
47+
!CreditCardUtil.isCreditCardNumberValid(cardNumber) -> {
48+
throw IllegalArgumentException("Card number is invalid")
49+
}
50+
51+
!CreditCardUtil.isCreditCardExpirationDateValid(expiryMonth, expiryYear) -> {
52+
throw IllegalArgumentException("Card expiration date is invalid")
53+
}
54+
55+
!CreditCardUtil.isCreditCardCVNValid(cvn) -> {
56+
throw IllegalArgumentException("Card CVN is invalid")
57+
}
58+
}
59+
60+
val deviceFingerprint = getFingerprint("collect_card_data")
61+
val request = CardsRequestDto(
62+
cardNumber = cardNumber,
63+
expiryMonth = expiryMonth,
64+
expiryYear = expiryYear,
65+
cvn = cvn,
66+
cardholderFirstName = cardholderFirstName,
67+
cardholderLastName = cardholderLastName,
68+
cardholderEmail = cardholderEmail,
69+
cardholderPhoneNumber = cardholderPhoneNumber,
70+
paymentSessionId = paymentSessionId,
71+
device = DeviceFingerprint(deviceFingerprint)
72+
)
73+
74+
val authToken = AuthTokenGenerator.generateAuthToken(apiKey)
75+
val response = client.paymentWithSession(request, authToken)
76+
_state.update { it.copy(isLoading = false, cardResponse = response) }
77+
return response
78+
} catch (e: CardsSessionException) {
79+
logger.e("API request failed: ${e.message}")
80+
_state.update { CardSessionState(isLoading = false, exception = e) }
81+
return CardsResponseDto(message = e.message ?: "Unknown error")
82+
} catch (e: Exception) {
83+
logger.e("API request failed: ${e.message}")
84+
_state.update {
85+
CardSessionState(
86+
isLoading = false,
87+
exception = CardsSessionException(errorCode = UNKNOWN_ERROR, e.message ?: "Unknown error")
88+
)
89+
}
90+
return CardsResponseDto(message = e.message ?: "Unknown error")
91+
}
92+
}
93+
94+
override suspend fun collectCvn(
95+
cvn: String,
96+
paymentSessionId: String
97+
): CardsResponseDto {
98+
_state.update { it.copy(isLoading = true, exception = null) }
99+
100+
try {
101+
if (!CreditCardUtil.isCreditCardCVNValid(cvn)) {
102+
throw IllegalArgumentException("Card CVN is invalid")
103+
}
104+
105+
val deviceFingerprint = getFingerprint("collect_cvn")
106+
val request = CardsRequestDto(
107+
cvn = cvn,
108+
paymentSessionId = paymentSessionId,
109+
device = DeviceFingerprint(deviceFingerprint)
110+
)
111+
112+
val authToken = AuthTokenGenerator.generateAuthToken(apiKey)
113+
val response = client.paymentWithSession(request, authToken)
114+
_state.update { it.copy(isLoading = false, cardResponse = response) }
115+
return response
116+
} catch (e: CardsSessionException) {
117+
logger.e("API request failed: ${e.message}")
118+
_state.update { CardSessionState(isLoading = false, exception = e) }
119+
return CardsResponseDto(message = e.message ?: "Unknown error")
120+
} catch (e: Exception) {
121+
logger.e("API request failed: ${e.message}")
122+
_state.update {
123+
CardSessionState(
124+
isLoading = false,
125+
exception = CardsSessionException(errorCode = UNKNOWN_ERROR, e.message ?: "Unknown error")
126+
)
127+
}
128+
return CardsResponseDto(message = e.message ?: "Unknown error")
129+
}
130+
}
131+
132+
private fun getFingerprint(eventName: String): String {
133+
// TODO: Implement iOS-specific device fingerprinting
134+
return ""
135+
}
136+
137+
companion object {
138+
fun create(apiKey: String): CardSessions {
139+
Logger("").debugBuild()
140+
return CardSessionsImpl(
141+
apiKey = apiKey,
142+
httpClient = HttpClientFactory().create()
143+
)
144+
}
145+
}
146+
}

cardsSdk/src/iosMain/kotlin/com/cards/session/cards/sdk/CardSessionsIos.kt

Lines changed: 0 additions & 7 deletions
This file was deleted.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.cards.session.util
2+
3+
import io.github.aakira.napier.DebugAntilog
4+
import io.github.aakira.napier.Napier
5+
6+
actual class Logger actual constructor(private val tag: String) {
7+
actual fun d(message: String, throwable: Throwable?) {
8+
Napier.d(message = "$message${throwable?.let { "\n$it" } ?: ""}", tag = tag)
9+
}
10+
11+
actual fun i(message: String, throwable: Throwable?) {
12+
Napier.i(message = "$message${throwable?.let { "\n$it" } ?: ""}", tag = tag)
13+
}
14+
15+
actual fun w(message: String, throwable: Throwable?) {
16+
Napier.w(message = "$message${throwable?.let { "\n$it" } ?: ""}", tag = tag)
17+
}
18+
19+
actual fun e(message: String, throwable: Throwable?) {
20+
Napier.e(message = "$message${throwable?.let { "\n$it" } ?: ""}", tag = tag)
21+
}
22+
23+
actual fun debugBuild() {
24+
Napier.base(DebugAntilog())
25+
}
26+
}

0 commit comments

Comments
 (0)