Skip to content

Commit

Permalink
Merge pull request #86 from cryptimeleon/feature/app-checkout
Browse files Browse the repository at this point in the history
Feature/app checkout
  • Loading branch information
this-kramer authored Apr 21, 2022
2 parents 261af8a + a3d6e9f commit 0d6f810
Show file tree
Hide file tree
Showing 78 changed files with 2,067 additions and 365 deletions.
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

0 comments on commit 0d6f810

Please sign in to comment.