Skip to content

Commit

Permalink
Merge pull request #5 from primer-io/vm/drop-in-example_v2
Browse files Browse the repository at this point in the history
Drop In Checkout example app
  • Loading branch information
vasi-twks authored Mar 26, 2024
2 parents a133e54 + 3889952 commit ab8683e
Show file tree
Hide file tree
Showing 52 changed files with 1,524 additions and 78 deletions.
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
package io.primer.checkout.cobadged.configuration.ui

import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
Expand All @@ -27,6 +32,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import io.primer.checkout.cobadged.R
import io.primer.checkout.cobadged.configuration.viewmodel.CheckoutConfigurationViewModel

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CheckoutConfigurationScreen(
onNavigateToCheckout: (String) -> Unit,
Expand All @@ -35,87 +41,112 @@ fun CheckoutConfigurationScreen(
) {
val checkoutUiState by viewModel.checkoutUiState.collectAsState()

Column(
modifier
.verticalScroll(rememberScrollState())
.padding(dimensionResource(id = R.dimen.default_margin))
) {
Text(
text = stringResource(id = R.string.settings_title),
modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.headlineMedium
)

Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.spacing_half)))

Text(
text = stringResource(id = R.string.settings_description),
modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.bodyMedium
)

Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.spacing_double)))

OutlinedTextField(
value = checkoutUiState.clientTokenState.clientToken,
onValueChange = viewModel::updateClientToken,
modifier = modifier
.fillMaxWidth()
.height(height = dimensionResource(id = R.dimen.client_token_field_min_height)),
label = { Text(text = stringResource(id = R.string.client_token)) },
placeholder = { Text(text = stringResource(id = R.string.client_token)) }
)

Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.spacing_default)))

Divider(color = colorResource(id = R.color.black), thickness = 0.5.dp)

Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.spacing_default)))

OutlinedTextField(
value = checkoutUiState.serverUrl,
onValueChange = viewModel::updateServerUrl,
modifier = modifier.fillMaxWidth(),
label = { Text(text = stringResource(id = R.string.server_url)) },
placeholder = { Text(text = stringResource(id = R.string.server_url_hint)) }
)

Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.spacing_half)))

Button(
onClick = { viewModel.requestNewClientToken() },
modifier = modifier
.align(Alignment.CenterHorizontally)
.fillMaxWidth(),
enabled = checkoutUiState.serverUrl.isNotBlank()
) {
Text(text = stringResource(id = R.string.request_client_token))
Scaffold(
modifier = modifier
.fillMaxSize()
.background(color = MaterialTheme.colorScheme.onSurface),
topBar = {
TopAppBar(
title = {
Text(
text = stringResource(id = R.string.main_title)
)
},
modifier = Modifier.fillMaxWidth()
)
}

Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.spacing_default)))

Button(
onClick = { onNavigateToCheckout.invoke(checkoutUiState.clientTokenState.clientToken) },
modifier = modifier
.align(Alignment.CenterHorizontally)
.fillMaxWidth(),
enabled = checkoutUiState.clientTokenState.isValid
) { contentPaddings ->
Column(
modifier
.verticalScroll(rememberScrollState())
.padding(contentPaddings)
.padding(dimensionResource(id = R.dimen.default_margin))
) {
Text(text = stringResource(id = R.string.open_checkout))
}
Text(
text = stringResource(id = R.string.settings_title),
modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.headlineMedium
)

Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.spacing_half)))

Text(
text = stringResource(id = R.string.settings_description),
modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.bodyMedium
)

Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.spacing_double)))

OutlinedTextField(
value = checkoutUiState.clientTokenState.clientToken,
onValueChange = viewModel::updateClientToken,
modifier = modifier
.fillMaxWidth()
.height(height = dimensionResource(id = R.dimen.client_token_field_min_height)),
label = { Text(text = stringResource(id = R.string.client_token)) },
placeholder = { Text(text = stringResource(id = R.string.client_token)) }
)

Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.spacing_default)))

Divider(color = colorResource(id = R.color.black), thickness = 0.5.dp)

Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.spacing_default)))

OutlinedTextField(
value = checkoutUiState.serverUrl,
onValueChange = viewModel::updateServerUrl,
modifier = modifier.fillMaxWidth(),
label = { Text(text = stringResource(id = R.string.server_url)) },
placeholder = { Text(text = stringResource(id = R.string.server_url_hint)) }
)

Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.spacing_half)))

Button(
onClick = { viewModel.requestNewClientToken() },
modifier = modifier
.align(Alignment.CenterHorizontally)
.fillMaxWidth(),
enabled = checkoutUiState.serverUrl.isNotBlank()
) {
Text(text = stringResource(id = R.string.request_client_token))
}

checkoutUiState.errorMessage?.let { errorMessage ->
Column {
Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.spacing_default)))
Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.spacing_default)))

Button(
onClick = {
onNavigateToCheckout.invoke(checkoutUiState.clientTokenState.clientToken)
},
modifier = modifier
.align(Alignment.CenterHorizontally)
.fillMaxWidth(),
enabled = checkoutUiState.clientTokenState.isValid
) {
Text(text = stringResource(id = R.string.open_checkout))
}

OutlinedCard(
modifier = modifier.fillMaxWidth(),
border = BorderStroke(0.5.dp, color = MaterialTheme.colorScheme.error)
) {
Text(
text = errorMessage,
modifier = modifier.padding(dimensionResource(id = R.dimen.default_margin))
checkoutUiState.errorMessage?.let { errorMessage ->
Column {
Spacer(
modifier = Modifier.height(
dimensionResource(id = R.dimen.spacing_default)
)
)

OutlinedCard(
modifier = modifier.fillMaxWidth(),
border = BorderStroke(0.5.dp, color = MaterialTheme.colorScheme.error)
) {
Text(
text = errorMessage,
modifier = modifier.padding(
dimensionResource(id = R.dimen.default_margin)
)
)
}
}
}
}
Expand Down
1 change: 1 addition & 0 deletions co-badged-cards/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<resources>
<string name="app_name">Co-badged cards</string>
<string name="main_title">Co-badged cards Example App</string>
<string name="settings_title">Configure the example app</string>
<string name="settings_description">To initiate the checkout process, input your manually generated client token or input the address of the server you\'ve created using the guidelines outlined in our README.</string>
<string name="open_checkout">Go to checkout</string>
Expand Down
7 changes: 7 additions & 0 deletions drop-in-checkout/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/build

# security
*.pem
*.crt
.env/
.env.example
69 changes: 69 additions & 0 deletions drop-in-checkout/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# 💳 DropIn Checkout Example

This example demonstrates how to integrate support for DropIn Checkout in your Android app.

## Getting Started

To run the example app:

1. Clone the repo:
```sh
git clone https://github.com/primer-io/checkout-examples-android.git
```
2. Open the project in Android Studio 🚀

Select `drop-in-checkout` configuration and run the project.

3. Setup the client token server

Refer to the instructions provided in the [example-backend Readme](https://github.com/primer-io/checkout-example-backend/blob/main/README.md)
to set up the server for generating the client token needed to initialize the SDK.

----

This project requires a server to communicate with Primer's API. To get started quickly, we encourage you to use the [companion backend](https://github.com/primer-io/checkout-example-backend).

#### Setting Checkout Backend URL

- You can set the URL of the checkout backend to initiate the checkout generated in step 3.
- The application provides an input field where you can input this URL or you can set the `BASE_URL` field defined
in the [ClientSessionService](src/main/java/io/primer/checkout/cobadged/configuration/data/api/ClientSessionService.kt#L26).
- When the URL is set, you can request new client token in the example app.

#### Manually Created Client Token

- On the initial screen of your application, there's an option to manually input a client token.
- Paste the client token generated specifically for your integration to start the checkout process.

## Understanding the integration

We have followed a very simple Android architectural principles as describe in Android [documentation](https://developer.android.com/topic/architecture).

We have used following stack:

- Hilt for DI
- Retrofit for API calls
- Jetpack Compose + ViewModels on the UI/presentation layer

For easier separation of concerns, application was split into:

### Repositories

We have organized our code into two repositories to streamline the integration process:

#### 1. Primer DropIn Initialization and Events

- [PrimerDropInRepository](src/main/java/io/primer/checkout/dropin/checkout/data/repository/PrimerDropInRepository.kt)
contains the necessary code for initializing the Primer Universal Checkout SDK and managing checkout lifecycle events.
- Use this repository to set up the base structure and manage Primer Universal Checkout events within your application.


### UI/Presentation

We have organized our code into two ViewModels to streamline the integration process:

#### 1. Primer DropIn Initialization and Events

- [CheckoutDropInViewModel](src/main/java/io/primer/checkout/cobadged/configuration/viewmodel/CheckoutConfigurationViewModel.kt)
focuses on retrieving and validating client token needed for SDK initialization.

91 changes: 91 additions & 0 deletions drop-in-checkout/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import com.android.build.api.dsl.Packaging

plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp)
alias(libs.plugins.hilt)
}

android {
namespace = "io.primer.checkout.dropin"
compileSdk = 34

defaultConfig {
applicationId = "io.primer.checkout.dropin"
minSdk = 21
targetSdk = 34
versionCode = 1
versionName = "1.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
}

buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()

// Enable Coroutines and Flow APIs
freeCompilerArgs =
freeCompilerArgs + "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
freeCompilerArgs = freeCompilerArgs + "-Xopt-in=kotlinx.coroutines.FlowPreview"
}
buildFeatures {
compose = true
buildConfig = true
}
composeOptions {
kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get()
}
// Multiple dependency bring these files in. Exclude them to enable
// our test APK to build (has no effect on our AARs)
fun Packaging.() {
resources.excludes += "/META-INF/AL2.0"
resources.excludes += "/META-INF/LGPL2.1"
}
}

dependencies {
ksp(libs.hilt.android.compiler)

implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.navigation.compose)
implementation(libs.material)
implementation(libs.okhttp3.logging.interceptor)
implementation(libs.retrofit2.converter.kotlinx.serialization)
implementation(libs.retrofit2)
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.hilt.android)
implementation(libs.hilt.navigation.compose)

implementation(libs.primer.sdk)
implementation(libs.primer.threeds.sdk)

testImplementation(libs.junit)
}

apply {
from("$rootDir/config/lint.gradle")
}
Loading

0 comments on commit ab8683e

Please sign in to comment.