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

Feature/app checkout #86

Merged
merged 32 commits into from
Apr 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
cf266b1
Add default metadata to promotions
this-kramer Feb 23, 2022
2989b8c
Add use case and state for the current state for a promotion.
this-kramer Feb 23, 2022
93dfa70
Allow fast earn for VIP promotion
this-kramer Feb 23, 2022
ec085af
Fix now method in StreakTokenUpdate and add test
this-kramer Feb 23, 2022
ae52aa5
Fix test of VIP promotion to allow fast earn
this-kramer Feb 23, 2022
43c452d
Add draft for user selection of token updates in basket.
this-kramer Feb 23, 2022
8c7f2f0
Store user choices for token updates to database and analyze basket w…
this-kramer Feb 24, 2022
3a3ab31
Add bulk spend/earn api, remove old api and add locking mechanism to …
this-kramer Mar 2, 2022
56061ca
Fix computation of basket value to use promotion
this-kramer Mar 2, 2022
47ec757
Simplify the dev pay endpoint.
this-kramer Mar 2, 2022
bf88c5a
Update app data / domain to support bulk (earn/spend) updates
this-kramer Mar 10, 2022
49dee12
Add instructions on how to run integration tests and comments
this-kramer Mar 25, 2022
b76d1e8
Fix test cases/payment api
this-kramer Mar 25, 2022
5ffc80a
Update mcl
this-kramer Mar 25, 2022
8ab1c3d
Add some logging to batch proof at service
this-kramer Mar 25, 2022
e9b0060
Extend api of repositories, some bugfixes in REST APIs
this-kramer Mar 25, 2022
39f3a3b
Use correct vector index for VIP status in dashboard ui
this-kramer Mar 25, 2022
2baf172
Add simple checkout ui that is opened when user pays basket
this-kramer Mar 25, 2022
0240b59
Finish PayAndRedeemUseCase
this-kramer Mar 25, 2022
397f6ec
Work on checkout ui, simplify some dataclasses
this-kramer Apr 3, 2022
8796a0a
Move reward choice to new screen
this-kramer Apr 4, 2022
1ad4abd
Add model to checkout to prepare having user-centered texts
this-kramer Apr 8, 2022
a0cc8bd
Add data model for checkout screen
this-kramer Apr 13, 2022
eef470e
Work on checkout ui, reduce font size in dashboard
this-kramer Apr 13, 2022
2bd7427
Add test for timestamp serialization to cover long bug
this-kramer Apr 13, 2022
48c93bd
Remove resolved todo in crypto
this-kramer Apr 13, 2022
d59112d
Minor UI tweaks for checkout UI
this-kramer Apr 13, 2022
b2734c5
Fix long represenation by math version bump and adapt test
this-kramer Apr 14, 2022
b125712
Mini-ui fixes in app
this-kramer Apr 20, 2022
48c6a60
Add documentation as requested in review
this-kramer Apr 20, 2022
9d8e76b
Minor refactoring according to review
this-kramer Apr 20, 2022
a3d6e9f
Update changelog
this-kramer Apr 21, 2022
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Next Release

- Add checkout to app, lock mechanism to server and change server api to accept bulk requests [#86](https://github.com/cryptimeleon/incentive-system/pull/86)
- Extract client-side pseudorandomness and include promotionId to PRF input [#76](https://github.com/cryptimeleon/incentive-system/pull/76)
- Add streak and VIP promotion, polishing of the promotion api [71](https://github.com/cryptimeleon/incentive-system/pull/71)
- Integrate promotions to app, ui updates, and project refactoring [#70](https://github.com/cryptimeleon/incentive-system/pull/70)
Expand Down
5 changes: 5 additions & 0 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ plugins {
id 'kotlin-kapt' // Without this databinding does not work correctly
id 'dagger.hilt.android.plugin'
id 'kotlin-parcelize'
id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlin_version"
}


Expand Down Expand Up @@ -77,6 +78,10 @@ dependencies {
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"

// Nice kotlin serialization with polymorphism
implementation "org.jetbrains.kotlinx:kotlinx-serialization-core:1.3.2"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2"

// Support for activityViewModel to allow shared view models
implementation "androidx.activity:activity-ktx:$activity_version"
implementation "androidx.fragment:fragment-ktx:$fragment_version"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import org.cryptimeleon.incentive.app.ui.basket.BasketUi
import org.cryptimeleon.incentive.app.ui.benchmark.BenchmarkUi
import org.cryptimeleon.incentive.app.ui.checkout.CheckoutUi
import org.cryptimeleon.incentive.app.ui.dashboard.Dashboard
import org.cryptimeleon.incentive.app.ui.rewards.RewardsUi
import org.cryptimeleon.incentive.app.ui.scan.ScanScreen
import org.cryptimeleon.incentive.app.ui.settings.Settings
import org.cryptimeleon.incentive.app.ui.setup.SetupUi
Expand All @@ -25,6 +27,8 @@ object MainDestination {
const val DASHBOARD_ROUTE = "dashboard"
const val SCANNER_ROUTE = "scanner"
const val BASKET_ROUTE = "basket"
const val REWARDS_ROUTE = "rewards"
const val CHECKOUT_ROUTE = "checkout"
const val SETTINGS_ROUTE = "settings"
const val BENCHMARK_ROUTE = "benchmark"
}
Expand Down Expand Up @@ -75,9 +79,16 @@ fun NavGraph(
composable(MainDestination.BASKET_ROUTE) {
BasketUi(
actions.openSettings,
actions.openBenchmark
actions.openBenchmark,
actions.openRewards
)
}
composable(MainDestination.REWARDS_ROUTE) {
RewardsUi(actions.openCheckout)
}
composable(MainDestination.CHECKOUT_ROUTE) {
CheckoutUi(actions.navigateToDashboard)
}
composable(MainDestination.SETTINGS_ROUTE) { Settings(actions.onExitSettings) }
composable(MainDestination.BENCHMARK_ROUTE) { BenchmarkUi(actions.onExitBenchmark) }
}
Expand All @@ -103,6 +114,18 @@ class MainActions(navController: NavHostController) {
val openBenchmark: () -> Unit = {
navController.navigate(MainDestination.BENCHMARK_ROUTE)
}

val openCheckout: () -> Unit = {
navController.navigate(MainDestination.CHECKOUT_ROUTE)
}

val openRewards: () -> Unit = {
navController.navigate(MainDestination.REWARDS_ROUTE)
}

val navigateToDashboard: () -> Unit = {
navController.navigate(MainDestination.DASHBOARD_ROUTE)
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import org.cryptimeleon.incentive.app.data.database.basket.BasketItemEntity
import org.cryptimeleon.incentive.app.data.database.basket.ShoppingItemEntity
import org.cryptimeleon.incentive.app.data.network.BasketApiService
import org.cryptimeleon.incentive.app.data.network.NetworkBasketItem
import org.cryptimeleon.incentive.app.data.network.NetworkPayBody
import org.cryptimeleon.incentive.app.data.network.NetworkShoppingItem
import org.cryptimeleon.incentive.app.domain.IBasketRepository
import org.cryptimeleon.incentive.app.domain.model.Basket
import org.cryptimeleon.incentive.app.domain.model.BasketItem
import org.cryptimeleon.incentive.app.domain.model.ShoppingItem
import timber.log.Timber

class BasketRepository(
private val basketApiService: BasketApiService,
Expand Down Expand Up @@ -134,20 +134,18 @@ class BasketRepository(
if (basket != null) {
basketApiService.deleteBasket(basket.basketId)
}
basketDao.deleteAllBasketItems()
return createNewBasket()
}

override suspend fun payCurrentBasket(): Boolean {
val basket = basket.first() ?: return false
override suspend fun payCurrentBasket() {
val basket = basket.first()

// Pay basket
val payResponse =
basketApiService.payBasket(NetworkPayBody(basket.basketId, basket.value))
return if (payResponse.isSuccessful) {
discardCurrentBasket()
true
} else {
false
basketApiService.payBasket(basket!!.basketId)
if (!payResponse.isSuccessful) {
Timber.e(payResponse.raw().toString())
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import org.cryptimeleon.craco.sig.sps.eq.SPSEQSignature
import org.cryptimeleon.incentive.app.data.database.crypto.CryptoDao
import org.cryptimeleon.incentive.app.data.database.crypto.CryptoMaterialEntity
import org.cryptimeleon.incentive.app.data.database.crypto.CryptoTokenEntity
import org.cryptimeleon.incentive.app.data.network.CryptoApiService
import org.cryptimeleon.incentive.app.data.network.InfoApiService
import org.cryptimeleon.incentive.app.domain.ICryptoRepository
import org.cryptimeleon.incentive.app.domain.model.BulkRequestDto
import org.cryptimeleon.incentive.app.domain.model.BulkResponseDto
import org.cryptimeleon.incentive.app.domain.model.CryptoMaterial
import org.cryptimeleon.incentive.crypto.IncentiveSystem
import org.cryptimeleon.incentive.crypto.model.IncentivePublicParameters
Expand All @@ -23,9 +24,7 @@ import org.cryptimeleon.incentive.crypto.model.keys.user.UserPublicKey
import org.cryptimeleon.incentive.crypto.model.keys.user.UserSecretKey
import org.cryptimeleon.incentive.crypto.model.messages.JoinResponse
import org.cryptimeleon.math.serialization.converter.JSONConverter
import org.cryptimeleon.math.structures.cartesian.Vector
import timber.log.Timber
import java.math.BigInteger
import java.util.*

/**
Expand Down Expand Up @@ -62,17 +61,18 @@ class CryptoRepository(
val userKeyPair = cryptoMaterial.ukp
val incentiveSystem = IncentiveSystem(pp)

val joinRequest = incentiveSystem.generateJoinRequest(providerPublicKey, userKeyPair, promotionParameters)
val joinRequest =
incentiveSystem.generateJoinRequest(providerPublicKey, userKeyPair, promotionParameters)
val joinResponse = cryptoApiService.runIssueJoin(
jsonConverter.serialize(joinRequest.representation),
promotionParameters.promotionId.toString(),
jsonConverter.serialize(userKeyPair.pk.representation)
)

if (!joinResponse.isSuccessful) {
Timber.e(joinResponse.raw().toString())
throw RuntimeException(
"Join Response not successful: " + joinResponse.code() + "\n" + joinResponse.errorBody()!!
.string()
"Join not successful"
)
}

Expand All @@ -89,45 +89,33 @@ class CryptoRepository(
}
}

override suspend fun runCreditEarn(
override suspend fun sendTokenUpdatesBatch(
basketId: UUID,
promotionParameters: PromotionParameters,
basketValue: Int
bulkRequestDto: BulkRequestDto
) {
val cryptoMaterial = cryptoMaterial.first()!!
val token = tokens.first().find { it.promotionId == promotionParameters.promotionId }
val pp = cryptoMaterial.pp
val providerPublicKey = cryptoMaterial.ppk
val userKeyPair = cryptoMaterial.ukp
val incentiveSystem = IncentiveSystem(pp)

val earnRequest =
incentiveSystem.generateEarnRequest(token, providerPublicKey, userKeyPair)
val earnResponse = cryptoApiService.runCreditEarn(
basketId,
promotionParameters.promotionId.toInt(),
jsonConverter.serialize(earnRequest.representation)
)
val response = cryptoApiService.sendTokenUpdatesBatch(basketId, bulkRequestDto)
if (!response.isSuccessful) {
Timber.e(response.raw().toString())
throw RuntimeException(response.errorBody().toString())
}
}

Timber.i("Earn response $earnResponse")
override suspend fun retrieveTokenUpdatesResults(basketId: UUID): BulkResponseDto {
val response = cryptoApiService.retrieveTokenUpdatesResults(basketId)
if (!response.isSuccessful || response.body() == null) {
Timber.e(response.raw().toString())
throw RuntimeException(response.errorBody().toString())
}
return response.body()!!
}

// The basket service computes the value in the backend, so no need to send it over the wire
val newToken = incentiveSystem.handleEarnRequestResponse(
promotionParameters,
earnRequest,
SPSEQSignature(
jsonConverter.deserialize(earnResponse.body()),
pp.bg.g1,
pp.bg.g2
),
Vector.of(BigInteger.valueOf(basketValue.toLong())),
token,
providerPublicKey,
userKeyPair
override suspend fun putToken(promotionParameters: PromotionParameters, token: Token) {
cryptoDao.insertToken(
CryptoTokenEntity(
promotionParameters.promotionId.toInt(),
jsonConverter.serialize(token.representation)
)
)

cryptoDao.insertToken(toCryptoTokenEntity(newToken))
Timber.i("Added new token $newToken to database")
}

override suspend fun refreshCryptoMaterial(): Boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import org.cryptimeleon.incentive.app.data.database.promotion.PromotionDao
import org.cryptimeleon.incentive.app.data.database.promotion.PromotionEntity
import org.cryptimeleon.incentive.app.data.database.promotion.TokenUpdateUserChoiceEntity
import org.cryptimeleon.incentive.app.data.network.PromotionApiService
import org.cryptimeleon.incentive.app.domain.IPromotionRepository
import org.cryptimeleon.incentive.app.domain.model.PromotionUserUpdateChoice
import org.cryptimeleon.incentive.app.domain.model.UserUpdateChoice
import org.cryptimeleon.incentive.promotion.Promotion
import org.cryptimeleon.math.serialization.RepresentableRepresentation
import org.cryptimeleon.math.serialization.converter.JSONConverter
import java.math.BigInteger

class PromotionRepository(
private val promotionApiService: PromotionApiService,
Expand All @@ -27,6 +31,17 @@ class PromotionRepository(
}
}.flowOn(Dispatchers.IO)

override val userUpdateChoices: Flow<List<PromotionUserUpdateChoice>> =
promotionDao.observerUserTokenUpdateChoices()
.map { userUpdateChoiceEntities: List<TokenUpdateUserChoiceEntity> ->
userUpdateChoiceEntities.map {
PromotionUserUpdateChoice(
it.promotionId,
it.userUpdateChoice
)
}
}.flowOn(Dispatchers.IO)

override suspend fun reloadPromotions() {
val promotionsResponse = promotionApiService.getPromotions()
if (promotionsResponse.isSuccessful) {
Expand All @@ -43,4 +58,8 @@ class PromotionRepository(
throw RuntimeException("Could not load promotions!")
}
}

override suspend fun putUserUpdateChoice(promotionId: BigInteger, choice: UserUpdateChoice) {
promotionDao.putUserTokenUpdateChoice(TokenUpdateUserChoiceEntity(promotionId, choice))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.cryptimeleon.incentive.app.data.database

import androidx.room.TypeConverter
import com.google.gson.Gson
import java.math.BigInteger

class BigIntegerConverter {
companion object {
@JvmStatic
@TypeConverter
fun fromBigInteger(b: BigInteger): String = Gson().toJson(b)

@JvmStatic
@TypeConverter
fun toBigInteger(s: String): BigInteger = Gson().fromJson(s, BigInteger::class.java)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,10 @@ interface PromotionDao {

@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertPromotions(promotionEntities: List<PromotionEntity>)
}

@Query("SELECT * FROM `token-update-user-choices`")
fun observerUserTokenUpdateChoices(): Flow<List<TokenUpdateUserChoiceEntity>>

@Insert(onConflict = OnConflictStrategy.REPLACE)
fun putUserTokenUpdateChoice(tokenUpdateUserChoiceEntity: TokenUpdateUserChoiceEntity)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import androidx.room.Database
import androidx.room.RoomDatabase

@Database(
entities = [PromotionEntity::class],
entities = [PromotionEntity::class, TokenUpdateUserChoiceEntity::class],
version = 1,
exportSchema = false
)
abstract class PromotionDatabase : RoomDatabase() {
abstract fun promotionDatabaseDao(): PromotionDao
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.cryptimeleon.incentive.app.data.database.promotion

import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.TypeConverter
import androidx.room.TypeConverters
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.cryptimeleon.incentive.app.data.database.BigIntegerConverter
import org.cryptimeleon.incentive.app.domain.model.UserUpdateChoice
import org.cryptimeleon.incentive.app.domain.model.module
import java.math.BigInteger

private val json = Json { serializersModule = module }

@TypeConverters(value = [TokenUpdateUserChoiceEntity.UserUpdateChoiceConverter::class, BigIntegerConverter::class])
@Entity(tableName = "token-update-user-choices")
data class TokenUpdateUserChoiceEntity(
@PrimaryKey
val promotionId: BigInteger,
val userUpdateChoice: UserUpdateChoice
) {
// Converter for storing the choices in the room database
class UserUpdateChoiceConverter {
companion object {
@JvmStatic
@TypeConverter
fun fromChoice(userUpdateChoice: UserUpdateChoice): String =
json.encodeToString(userUpdateChoice)

@JvmStatic
@TypeConverter
fun toChoice(s: String): UserUpdateChoice = json.decodeFromString(s,)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ interface BasketApiService {

// This endpoint is for developing only and will be replaced by some payment process in the future
@POST("basket/pay-dev")
suspend fun payBasket(@Body networkPayBody: NetworkPayBody): Response<Unit>
suspend fun payBasket(@Header("basket-id") basketId: UUID): Response<Unit>

@GET("basket")
suspend fun getBasketContent(@Header("basketId") basketId: UUID): Response<NetworkBasket>
Expand Down
Loading