Skip to content

Commit

Permalink
Merge pull request #6 from xendit/finalizing-ios
Browse files Browse the repository at this point in the history
Finalizing ios
  • Loading branch information
ahmadAlfhajri authored Dec 9, 2024
2 parents 3183e1d + 521d2cc commit a655fc9
Show file tree
Hide file tree
Showing 10 changed files with 312 additions and 66 deletions.
60 changes: 54 additions & 6 deletions README.md
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

Expand All @@ -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`:

Expand All @@ -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(
Expand All @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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

Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ fun AppRoot() {
cardholderLastName = "Name",
cardholderEmail = "[email protected]",
cardholderPhoneNumber = "01231245242",
paymentSessionId = "session_id MUST be 27 chars"
paymentSessionId = "ps-1234567890abcdef12345678"
)
}
}
Expand All @@ -104,7 +104,7 @@ fun AppRoot() {
scope.launch {
cardSessions.collectCvn(
cvn = "123",
paymentSessionId = "1234567890"
paymentSessionId = "ps-1234567890abcdef12345678"
)
}
}
Expand Down
24 changes: 24 additions & 0 deletions cardsSdk/src/androidMain/kotlin/com/cards/session/util/Logger.kt
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() {
}
}
22 changes: 6 additions & 16 deletions cardsSdk/src/commonMain/kotlin/com/cards/session/util/Logger.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,10 @@ package com.cards.session.util

import io.github.aakira.napier.Napier

class Logger(private val tag: String) {
fun d(message: String, throwable: Throwable? = null) {
Napier.d(message, throwable, tag)
}

fun i(message: String, throwable: Throwable? = null) {
Napier.i(message, throwable, tag)
}

fun w(message: String, throwable: Throwable? = null) {
Napier.w(message, throwable, tag)
}

fun e(message: String, throwable: Throwable? = null) {
Napier.e(message, throwable, tag)
}
expect class Logger(tag: String) {
fun d(message: String, throwable: Throwable? = null)
fun i(message: String, throwable: Throwable? = null)
fun w(message: String, throwable: Throwable? = null)
fun e(message: String, throwable: Throwable? = null)
fun debugBuild()
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import platform.Foundation.NSBundle

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

// please check this logic
private fun isDebugBuild(): Boolean {
Expand Down
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()
)
}
}
}

This file was deleted.

26 changes: 26 additions & 0 deletions cardsSdk/src/iosMain/kotlin/com/cards/session/util/Logger.kt
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())
}
}
Loading

0 comments on commit a655fc9

Please sign in to comment.