Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Form API Experiment #10216

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions form-example/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
apply from: configs.androidApplication

apply plugin: 'com.emergetools.android'
apply plugin: 'com.google.firebase.appdistribution'
apply plugin: 'org.jetbrains.kotlin.plugin.serialization'
apply plugin: 'org.jetbrains.kotlin.plugin.compose'

emerge {
// Api token is implicitly set to the EMERGE_API_TOKEN env variable

size {
tag.set(System.getenv("EMERGE_TAG"))
}

vcs {
gitHub {
repoOwner.set("stripe")
repoName.set("stripe-android")
}
}
}

android {
defaultConfig {
applicationId "com.stripe.android.form.example"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

versionName VERSION_NAME
versionCode getVersionCode(versionName)
}

buildFeatures {
compose true
}

testOptions {
unitTests {
// Note: without this, all Robolectric tests using assets will fail.
includeAndroidResources = true
all {
maxHeapSize = "1024m"
}
}

kotlinOptions {
freeCompilerArgs += ["-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"]
}

managedDevices {
localDevices {
register("pixel2api33") {
device = "Pixel 2"
apiLevel = 33
systemImageSource = "aosp"
}
}
}
}
}

dependencies {
implementation project(":form")

implementation libs.androidx.activity
implementation libs.androidx.appCompat
implementation libs.androidx.coreKtx
implementation libs.androidx.lifecycle
implementation libs.compose.activity
implementation libs.compose.material
implementation libs.compose.materialIcons
implementation libs.compose.ui
implementation libs.compose.uiToolingPreview
implementation libs.compose.navigation
implementation libs.loggingInterceptor
implementation libs.material
implementation platform('androidx.compose:compose-bom:2024.04.01')
implementation 'androidx.compose.ui:ui-graphics'
implementation 'androidx.compose.material3:material3'
androidTestImplementation platform('androidx.compose:compose-bom:2024.04.01')

debugImplementation libs.compose.uiTestManifest
debugImplementation libs.compose.uiTooling
debugImplementation libs.leakCanary

testImplementation project(':screenshot-testing')

testImplementation testLibs.androidx.archCore
testImplementation testLibs.androidx.composeUi
testImplementation testLibs.androidx.core
testImplementation testLibs.androidx.fragment
testImplementation testLibs.androidx.junit
testImplementation testLibs.androidx.junitKtx
testImplementation testLibs.androidx.lifecycle
testImplementation testLibs.androidx.testRules
testImplementation testLibs.hamcrest
testImplementation testLibs.junit
testImplementation testLibs.json
testImplementation testLibs.kotlin.annotations
testImplementation testLibs.kotlin.coroutines
testImplementation testLibs.kotlin.junit
testImplementation testLibs.mockito.core
testImplementation testLibs.mockito.inline
testImplementation testLibs.mockito.kotlin
testImplementation testLibs.robolectric
testImplementation testLibs.truth
testImplementation testLibs.turbine
testImplementation testLibs.espresso.intents

testImplementation testLibs.espresso.core

androidTestImplementation testLibs.espresso.core
androidTestImplementation testLibs.androidx.composeUi
}
1 change: 1 addition & 0 deletions form-example/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
STRIPE_ANDROID_NAMESPACE=com.stripe.android.form.example
15 changes: 15 additions & 0 deletions form-example/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application>
<activity
android:name="com.stripe.form.example.ui.theme.MainActivity"
android:exported="true"
android:label="@string/title_activity_main"
android:theme="@style/Theme.Stripeandroid">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.stripe.form.example.ui.theme

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Text
import androidx.compose.material.Scaffold
import androidx.compose.material.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController

@Composable
internal fun IndexScreen(
navController: NavHostController,
) {
Scaffold(
topBar = {
TopAppBar(
title = {
Text("Index")
}
)
}
) { innerPadding ->
LazyColumn(
modifier = Modifier
.padding(innerPadding)
) {
item {
Text(
modifier = Modifier
.fillMaxWidth()
.clickable {
navController.navigate("cardInput")
}
.padding(16.dp),
text = "Card Input"
)
}

item {
Text(
modifier = Modifier
.fillMaxWidth()
.clickable {
navController.navigate("customForm")
}
.padding(16.dp),
text = "Custom Form"
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.stripe.form.example.ui.theme

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.stripe.form.example.ui.theme.cardinput.CardInputScreen
import com.stripe.form.example.ui.theme.cardinput.CardInputViewModel
import com.stripe.form.example.ui.theme.customform.CustomFormScreen
import com.stripe.form.example.ui.theme.customform.CustomFormViewModel
import com.stripe.form.example.ui.theme.ui.theme.StripeandroidTheme

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
StripeandroidTheme {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "index"
) {
composable("index") {
IndexScreen(
navController = navController
)
}
composable("cardInput") {
val viewModel: CardInputViewModel = viewModel()
CardInputScreen(
viewModel = viewModel,
navController = navController
)
}
composable("customForm") {
val viewModel: CustomFormViewModel = viewModel()
CustomFormScreen(
viewModel = viewModel,
navController = navController
)
}
}
}
}
}
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
StripeandroidTheme {
Greeting("Android")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.stripe.form.example.ui.theme.cardinput

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.material.Button
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Close
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import com.stripe.android.uicore.utils.collectAsState
import com.stripe.form.FormUI

@Composable
internal fun CardInputScreen(
navController: NavHostController,
viewModel: CardInputViewModel
) {
val state by viewModel.state.collectAsState()
val form = state.form

Scaffold(
topBar = {
TopAppBar(
title = {
Text("Card Input")
},
navigationIcon = {
IconButton(
onClick = {
navController.popBackStack()
}
) {
Icon(
imageVector = Icons.Rounded.Close,
contentDescription = null
)
}
},
windowInsets = WindowInsets.statusBars
)
}
) { innerPadding ->
Box(
modifier = Modifier
.padding(innerPadding)
) {
if (form == null) {
CircularProgressIndicator()
return@Box
}

Column(
modifier = Modifier
.fillMaxSize()
) {
FormUI(
modifier = Modifier
.weight(1f, fill = false),
form = form
)

Button(
modifier = Modifier
.fillMaxWidth(),
onClick = {},
enabled = state.valid
) {
Text("Save")
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.stripe.form.example.ui.theme.cardinput

import androidx.lifecycle.ViewModel
import com.stripe.form.Key
import com.stripe.form.ValueChange
import com.stripe.form.buildForm
import com.stripe.form.fields.card.CardDetailsSpec
import com.stripe.form.find
import com.stripe.form.key
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update

class CardInputViewModel: ViewModel() {
private val _state = MutableStateFlow(State())
val state: StateFlow<State> = _state
val key = key<CardDetailsSpec.Output>()

init {
val form = buildForm(
onValuesChanged = ::onFormValuesChanged
) {
cardDetails(
key = key,
)
}
_state.update {
it.copy(form = form)
}
}

private fun onFormValuesChanged(data: Map<Key<*>, ValueChange<*>?>) {
val output = data.find(key)

_state.update {
it.copy(
valid = output?.isComplete ?: false,
)
}

}
}
Loading
Loading