-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #6 from xendit/finalizing-ios
Finalizing ios
- Loading branch information
Showing
10 changed files
with
312 additions
and
66 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
# Xendit Cards SDK | ||
|
||
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. | ||
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. | ||
|
||
## Features | ||
|
||
|
@@ -12,7 +12,7 @@ A lightweight SDK for integrating card payments into Android applications. This | |
|
||
## Installation | ||
|
||
### Gradle | ||
### Android - Gradle | ||
|
||
Add the following to your app's `build.gradle.kts`: | ||
|
||
|
@@ -26,17 +26,24 @@ dependencies { | |
|
||
### Initialize the SDK | ||
|
||
First, initialize the CardSessions instance with your Xendit public key: | ||
|
||
#### Android | ||
```kotlin | ||
val cardSessions = CardSessions.create( | ||
context = context, | ||
apiKey = "xnd_public_development_YOUR_KEY_HERE" | ||
) | ||
``` | ||
|
||
#### iOS | ||
```swift | ||
let cardSessions = CardSessionsFactory().create( | ||
apiKey: "xnd_public_development_YOUR_KEY_HERE" | ||
) | ||
``` | ||
|
||
### Collect Card Data | ||
|
||
#### Android | ||
```kotlin | ||
// Collect complete card information | ||
val response = cardSessions.collectCardData( | ||
|
@@ -52,21 +59,45 @@ val response = cardSessions.collectCardData( | |
) | ||
``` | ||
|
||
#### iOS | ||
```swift | ||
// Using async/await | ||
let response = try await cardSessions.collectCardData( | ||
cardNumber: "4000000000001091", | ||
expiryMonth: "12", | ||
expiryYear: "2025", | ||
cvn: "123", // Optional | ||
cardholderFirstName: "John", | ||
cardholderLastName: "Doe", | ||
cardholderEmail: "[email protected]", | ||
cardholderPhoneNumber: "+1234567890", | ||
paymentSessionId: "ps-1234567890" | ||
) | ||
``` | ||
|
||
### Collect CVN Only | ||
|
||
For saved cards where you only need to collect the CVN: | ||
|
||
#### Android | ||
```kotlin | ||
val response = cardSessions.collectCvn( | ||
cvn = "123", | ||
paymentSessionId = "ps-1234567890" | ||
) | ||
``` | ||
|
||
### Monitor Session State | ||
#### iOS | ||
```swift | ||
let response = try await cardSessions.collectCvn( | ||
cvn: "123", | ||
paymentSessionId: "ps-1234567890" | ||
) | ||
``` | ||
|
||
The SDK provides a StateFlow to monitor the current state of operations: | ||
### Monitor Session State | ||
|
||
#### Android | ||
```kotlin | ||
cardSessions.state.collect { state -> | ||
when { | ||
|
@@ -77,6 +108,21 @@ cardSessions.state.collect { state -> | |
} | ||
``` | ||
|
||
#### iOS | ||
```swift | ||
// Using Combine | ||
cardSessions.state | ||
.sink { state in | ||
if state.isLoading { | ||
// Show loading state | ||
} else if let error = state.error { | ||
// Handle error | ||
} else if let response = state.cardResponse { | ||
// Handle success | ||
} | ||
} | ||
``` | ||
|
||
## Response Types | ||
|
||
### Success Response | ||
|
@@ -107,4 +153,6 @@ The SDK automatically handles: | |
## Requirements | ||
|
||
- Android API level 21 or higher | ||
- iOS 13.0 or higher | ||
- Swift 5.5 or higher | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -92,7 +92,7 @@ fun AppRoot() { | |
cardholderLastName = "Name", | ||
cardholderEmail = "[email protected]", | ||
cardholderPhoneNumber = "01231245242", | ||
paymentSessionId = "session_id MUST be 27 chars" | ||
paymentSessionId = "ps-1234567890abcdef12345678" | ||
) | ||
} | ||
} | ||
|
@@ -104,7 +104,7 @@ fun AppRoot() { | |
scope.launch { | ||
cardSessions.collectCvn( | ||
cvn = "123", | ||
paymentSessionId = "1234567890" | ||
paymentSessionId = "ps-1234567890abcdef12345678" | ||
) | ||
} | ||
} | ||
|
24 changes: 24 additions & 0 deletions
24
cardsSdk/src/androidMain/kotlin/com/cards/session/util/Logger.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package com.cards.session.util | ||
|
||
import io.github.aakira.napier.Napier | ||
|
||
actual class Logger actual constructor(private val tag: String) { | ||
actual fun d(message: String, throwable: Throwable?) { | ||
Napier.d(message, throwable, tag) | ||
} | ||
|
||
actual fun i(message: String, throwable: Throwable?) { | ||
Napier.i(message, throwable, tag) | ||
} | ||
|
||
actual fun w(message: String, throwable: Throwable?) { | ||
Napier.w(message, throwable, tag) | ||
} | ||
|
||
actual fun e(message: String, throwable: Throwable?) { | ||
Napier.e(message, throwable, tag) | ||
} | ||
|
||
actual fun debugBuild() { | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
146 changes: 146 additions & 0 deletions
146
cardsSdk/src/iosMain/kotlin/com/cards/session/cards/sdk/CardSessionsImpl.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
package com.cards.session.cards.sdk | ||
|
||
import com.cards.session.cards.models.CardsRequestDto | ||
import com.cards.session.cards.models.CardsResponseDto | ||
import com.cards.session.cards.models.DeviceFingerprint | ||
import com.cards.session.cards.network.CardsSessionError.UNKNOWN_ERROR | ||
import com.cards.session.cards.network.CardsSessionException | ||
import com.cards.session.cards.network.KtorCardsClient | ||
import com.cards.session.cards.ui.CardSessionState | ||
import com.cards.session.network.HttpClientFactory | ||
import com.cards.session.util.AuthTokenGenerator | ||
import com.cards.session.util.Logger | ||
import com.cardsession.sdk.CreditCardUtil | ||
import io.ktor.client.HttpClient | ||
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( | ||
private val apiKey: String, | ||
private val httpClient: HttpClient | ||
) : CardSessions { | ||
private val TAG = "CardSessionsImpl" | ||
private val logger = Logger(TAG) | ||
private val client = KtorCardsClient(httpClient) | ||
private val _state = MutableStateFlow(CardSessionState()) | ||
override val state: StateFlow<CardSessionState> = _state.asStateFlow() | ||
|
||
override suspend fun collectCardData( | ||
cardNumber: String, | ||
expiryMonth: String, | ||
expiryYear: String, | ||
cvn: String?, | ||
cardholderFirstName: String, | ||
cardholderLastName: String, | ||
cardholderEmail: String, | ||
cardholderPhoneNumber: String, | ||
paymentSessionId: String | ||
): CardsResponseDto { | ||
_state.update { it.copy(isLoading = true, exception = null) } | ||
|
||
try { | ||
// Validate card data | ||
when { | ||
!CreditCardUtil.isCreditCardNumberValid(cardNumber) -> { | ||
throw IllegalArgumentException("Card number is invalid") | ||
} | ||
|
||
!CreditCardUtil.isCreditCardExpirationDateValid(expiryMonth, expiryYear) -> { | ||
throw IllegalArgumentException("Card expiration date is invalid") | ||
} | ||
|
||
!CreditCardUtil.isCreditCardCVNValid(cvn) -> { | ||
throw IllegalArgumentException("Card CVN is invalid") | ||
} | ||
} | ||
|
||
val deviceFingerprint = getFingerprint("collect_card_data") | ||
val request = CardsRequestDto( | ||
cardNumber = cardNumber, | ||
expiryMonth = expiryMonth, | ||
expiryYear = expiryYear, | ||
cvn = cvn, | ||
cardholderFirstName = cardholderFirstName, | ||
cardholderLastName = cardholderLastName, | ||
cardholderEmail = cardholderEmail, | ||
cardholderPhoneNumber = cardholderPhoneNumber, | ||
paymentSessionId = paymentSessionId, | ||
device = DeviceFingerprint(deviceFingerprint) | ||
) | ||
|
||
val authToken = AuthTokenGenerator.generateAuthToken(apiKey) | ||
val response = client.paymentWithSession(request, authToken) | ||
_state.update { it.copy(isLoading = false, cardResponse = response) } | ||
return response | ||
} catch (e: CardsSessionException) { | ||
logger.e("API request failed: ${e.message}") | ||
_state.update { CardSessionState(isLoading = false, exception = e) } | ||
return CardsResponseDto(message = e.message ?: "Unknown error") | ||
} catch (e: Exception) { | ||
logger.e("API request failed: ${e.message}") | ||
_state.update { | ||
CardSessionState( | ||
isLoading = false, | ||
exception = CardsSessionException(errorCode = UNKNOWN_ERROR, e.message ?: "Unknown error") | ||
) | ||
} | ||
return CardsResponseDto(message = e.message ?: "Unknown error") | ||
} | ||
} | ||
|
||
override suspend fun collectCvn( | ||
cvn: String, | ||
paymentSessionId: String | ||
): CardsResponseDto { | ||
_state.update { it.copy(isLoading = true, exception = null) } | ||
|
||
try { | ||
if (!CreditCardUtil.isCreditCardCVNValid(cvn)) { | ||
throw IllegalArgumentException("Card CVN is invalid") | ||
} | ||
|
||
val deviceFingerprint = getFingerprint("collect_cvn") | ||
val request = CardsRequestDto( | ||
cvn = cvn, | ||
paymentSessionId = paymentSessionId, | ||
device = DeviceFingerprint(deviceFingerprint) | ||
) | ||
|
||
val authToken = AuthTokenGenerator.generateAuthToken(apiKey) | ||
val response = client.paymentWithSession(request, authToken) | ||
_state.update { it.copy(isLoading = false, cardResponse = response) } | ||
return response | ||
} catch (e: CardsSessionException) { | ||
logger.e("API request failed: ${e.message}") | ||
_state.update { CardSessionState(isLoading = false, exception = e) } | ||
return CardsResponseDto(message = e.message ?: "Unknown error") | ||
} catch (e: Exception) { | ||
logger.e("API request failed: ${e.message}") | ||
_state.update { | ||
CardSessionState( | ||
isLoading = false, | ||
exception = CardsSessionException(errorCode = UNKNOWN_ERROR, e.message ?: "Unknown error") | ||
) | ||
} | ||
return CardsResponseDto(message = e.message ?: "Unknown error") | ||
} | ||
} | ||
|
||
private fun getFingerprint(eventName: String): String { | ||
// TODO: Implement iOS-specific device fingerprinting | ||
return "" | ||
} | ||
|
||
companion object { | ||
fun create(apiKey: String): CardSessions { | ||
Logger("").debugBuild() | ||
return CardSessionsImpl( | ||
apiKey = apiKey, | ||
httpClient = HttpClientFactory().create() | ||
) | ||
} | ||
} | ||
} |
7 changes: 0 additions & 7 deletions
7
cardsSdk/src/iosMain/kotlin/com/cards/session/cards/sdk/CardSessionsIos.kt
This file was deleted.
Oops, something went wrong.
26 changes: 26 additions & 0 deletions
26
cardsSdk/src/iosMain/kotlin/com/cards/session/util/Logger.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package com.cards.session.util | ||
|
||
import io.github.aakira.napier.DebugAntilog | ||
import io.github.aakira.napier.Napier | ||
|
||
actual class Logger actual constructor(private val tag: String) { | ||
actual fun d(message: String, throwable: Throwable?) { | ||
Napier.d(message = "$message${throwable?.let { "\n$it" } ?: ""}", tag = tag) | ||
} | ||
|
||
actual fun i(message: String, throwable: Throwable?) { | ||
Napier.i(message = "$message${throwable?.let { "\n$it" } ?: ""}", tag = tag) | ||
} | ||
|
||
actual fun w(message: String, throwable: Throwable?) { | ||
Napier.w(message = "$message${throwable?.let { "\n$it" } ?: ""}", tag = tag) | ||
} | ||
|
||
actual fun e(message: String, throwable: Throwable?) { | ||
Napier.e(message = "$message${throwable?.let { "\n$it" } ?: ""}", tag = tag) | ||
} | ||
|
||
actual fun debugBuild() { | ||
Napier.base(DebugAntilog()) | ||
} | ||
} |
Oops, something went wrong.