Add VIP selection page and related data models
This commit introduces a new VIP selection page (`VipSelPage.kt`) that allows users to choose between Premium and Standard membership plans.
Key changes include:
* **New VIP Selection UI:**
* `VipSelPage.kt`: Implements the UI for selecting VIP plans, displaying prices, and benefits.
* `SelfProfileAction.kt`: Updated to include a "Rave Premium" button alongside "Edit Profile".
* **Data Models for Membership:**
* `MembershipModels.kt`: Defines data classes for membership configuration (`MembershipConfigData`, `ConfigData`, `Member`, `Benefit`, `Good`), price models (`VipPriceModel`), page data models (`VipPageDataModel`), and request bodies (`ValidateProductRequestBody`, `ValidateData`).
* `VipModelMapper`: Provides functions to transform backend data into UI-friendly models for price and benefit display.
* **API Integration:**
* `RiderProAPI.kt`: Added new endpoints `getMembershipConfig` to fetch membership details and `validateAndroidProduct` for product validation.
* **Navigation:**
* `Navi.kt`: Added `VipSelPage` to the navigation routes.
* `ProfileV3.kt`: The "Rave Premium" button in the self profile action now navigates to the `VipSelPage`.
* **Theming:**
* `Colors.kt`: Added new color definitions for premium buttons, VIP benefit highlighting, and price card states (selected/unselected).
* **Assets:**
* `ic_member.webp`: New icon for the "Rave Premium" button.
This commit is contained in:
@@ -12,6 +12,9 @@ import com.aiosman.ravenow.data.ListContainer
|
||||
import com.aiosman.ravenow.data.Moment
|
||||
import com.aiosman.ravenow.data.Room
|
||||
import com.aiosman.ravenow.entity.ChatNotification
|
||||
import com.aiosman.ravenow.data.membership.MembershipConfigData
|
||||
import com.aiosman.ravenow.data.membership.ValidateData
|
||||
import com.aiosman.ravenow.data.membership.ValidateProductRequestBody
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.RequestBody
|
||||
@@ -256,6 +259,15 @@ data class RemoveAccountRequestBody(
|
||||
)
|
||||
|
||||
interface RaveNowAPI {
|
||||
@GET("membership/config")
|
||||
@retrofit2.http.Headers("X-Requires-Auth: true")
|
||||
suspend fun getMembershipConfig(): Response<DataContainer<MembershipConfigData>>
|
||||
|
||||
@POST("membership/android/product/validate")
|
||||
@retrofit2.http.Headers("X-Requires-Auth: true")
|
||||
suspend fun validateAndroidProduct(
|
||||
@Body body: ValidateProductRequestBody
|
||||
): Response<DataContainer<ValidateData>>
|
||||
@POST("register")
|
||||
suspend fun register(@Body body: RegisterRequestBody): Response<Unit>
|
||||
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
package com.aiosman.ravenow.data.membership
|
||||
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class MembershipConfigData(
|
||||
@SerializedName("id") val id: Int,
|
||||
@SerializedName("name") val name: String,
|
||||
@SerializedName("description") val description: String,
|
||||
@SerializedName("version") val version: String,
|
||||
@SerializedName("config_data") val configData: ConfigData,
|
||||
@SerializedName("createdAt") val createdAt: String,
|
||||
@SerializedName("updatedAt") val updatedAt: String,
|
||||
)
|
||||
|
||||
data class ConfigData(
|
||||
@SerializedName("members") val members: List<Member>
|
||||
)
|
||||
|
||||
data class Member(
|
||||
@SerializedName("name") val name: String,
|
||||
@SerializedName("benefits") val benefits: List<Benefit>,
|
||||
@SerializedName("goods") val goods: List<Good>,
|
||||
)
|
||||
|
||||
data class Benefit(
|
||||
@SerializedName("level") val level: Int,
|
||||
@SerializedName("name") val name: String,
|
||||
@SerializedName("value") val value: JsonElement,
|
||||
@SerializedName("order") val order: Int,
|
||||
)
|
||||
|
||||
data class Good(
|
||||
@SerializedName("description") val description: String,
|
||||
@SerializedName("discount") val discount: Double,
|
||||
@SerializedName("goods_id") val goodsId: String,
|
||||
@SerializedName("originalPrice") val originalPrice: Double,
|
||||
@SerializedName("period") val period: String,
|
||||
@SerializedName("price") val price: Double,
|
||||
)
|
||||
|
||||
data class ValidateProductRequestBody(
|
||||
@SerializedName("plan_id") val planId: String,
|
||||
@SerializedName("product_id") val productId: String,
|
||||
)
|
||||
|
||||
data class ValidateData(
|
||||
@SerializedName("isValid") val isValid: Boolean,
|
||||
@SerializedName("mapped") val mapped: Boolean,
|
||||
@SerializedName("hasUnfinishedOrder") val hasUnfinishedOrder: Boolean,
|
||||
)
|
||||
|
||||
data class VipPriceModel(
|
||||
val title: String,
|
||||
val proPrice: String,
|
||||
val standardPrice: String,
|
||||
val proDesc: String,
|
||||
val standardDesc: String,
|
||||
val id: String,
|
||||
val proGoodsId: String?,
|
||||
val standardGoodsId: String?,
|
||||
) {
|
||||
companion object {
|
||||
const val MONTH_ID = "monthly"
|
||||
const val YEAR_ID = "yearly"
|
||||
}
|
||||
}
|
||||
|
||||
data class VipPageDataModel(
|
||||
val title: String,
|
||||
val proHave: Boolean?,
|
||||
val proDesc: String,
|
||||
val standardHave: Boolean?,
|
||||
val standardDesc: String,
|
||||
val freeHave: Boolean?,
|
||||
val freeDesc: String,
|
||||
val order: Int,
|
||||
)
|
||||
|
||||
object VipModelMapper {
|
||||
fun generatePageDataList(members: List<Member>): List<VipPageDataModel> {
|
||||
if (members.size < 3) return emptyList()
|
||||
val free = members[0]
|
||||
val standard = members[1]
|
||||
val pro = members[2]
|
||||
|
||||
val names = (members.flatMap { it.benefits.map { b -> b.name } }).toSet().sorted()
|
||||
val list = names.map { name ->
|
||||
val freeB = free.benefits.firstOrNull { it.name == name }
|
||||
val stdB = standard.benefits.firstOrNull { it.name == name }
|
||||
val proB = pro.benefits.firstOrNull { it.name == name }
|
||||
|
||||
val order = proB?.order ?: stdB?.order ?: freeB?.order ?: 0
|
||||
VipPageDataModel(
|
||||
title = name,
|
||||
proHave = proB?.value?.asBooleanOrNull(),
|
||||
proDesc = proB?.value?.asStringOrEmpty() ?: "",
|
||||
standardHave = stdB?.value?.asBooleanOrNull(),
|
||||
standardDesc = stdB?.value?.asStringOrEmpty() ?: "",
|
||||
freeHave = freeB?.value?.asBooleanOrNull(),
|
||||
freeDesc = freeB?.value?.asStringOrEmpty() ?: "",
|
||||
order = order,
|
||||
)
|
||||
}
|
||||
return list.sortedBy { it.order }
|
||||
}
|
||||
|
||||
fun generatePriceDataList(members: List<Member>): List<VipPriceModel> {
|
||||
if (members.size < 3) return emptyList()
|
||||
val standard = members[1]
|
||||
val pro = members[2]
|
||||
|
||||
val list = mutableListOf<VipPriceModel>()
|
||||
// 首月(示例:如果后端 period = "first_month")
|
||||
val stdFirst = standard.goods.firstOrNull { it.period == "first_month" }
|
||||
val proFirst = pro.goods.firstOrNull { it.period == "first_month" }
|
||||
if (stdFirst != null && proFirst != null) {
|
||||
list.add(
|
||||
VipPriceModel(
|
||||
title = "首月",
|
||||
proPrice = proFirst.price.toInt().toString(),
|
||||
standardPrice = stdFirst.price.toInt().toString(),
|
||||
proDesc = proFirst.originalPrice.takeIf { it > 0 }?.toInt()?.toString() ?: "",
|
||||
standardDesc = stdFirst.originalPrice.takeIf { it > 0 }?.toInt()?.toString() ?: "",
|
||||
id = "first_month",
|
||||
proGoodsId = proFirst.goodsId,
|
||||
standardGoodsId = stdFirst.goodsId,
|
||||
)
|
||||
)
|
||||
}
|
||||
val stdMonth = standard.goods.firstOrNull { it.period == "month" }
|
||||
val proMonth = pro.goods.firstOrNull { it.period == "month" }
|
||||
if (stdMonth != null && proMonth != null) {
|
||||
list.add(
|
||||
VipPriceModel(
|
||||
title = "月付",
|
||||
proPrice = proMonth.price.toInt().toString(),
|
||||
standardPrice = stdMonth.price.toInt().toString(),
|
||||
proDesc = proMonth.originalPrice.takeIf { it > 0 }?.toInt()?.toString() ?: "",
|
||||
standardDesc = stdMonth.originalPrice.takeIf { it > 0 }?.toInt()?.toString() ?: "",
|
||||
id = VipPriceModel.MONTH_ID,
|
||||
proGoodsId = proMonth.goodsId,
|
||||
standardGoodsId = stdMonth.goodsId,
|
||||
)
|
||||
)
|
||||
}
|
||||
// 半年
|
||||
val stdHalf = standard.goods.firstOrNull { it.period == "half_year" }
|
||||
val proHalf = pro.goods.firstOrNull { it.period == "half_year" }
|
||||
if (stdHalf != null && proHalf != null) {
|
||||
list.add(
|
||||
VipPriceModel(
|
||||
title = "半年",
|
||||
proPrice = proHalf.price.toInt().toString(),
|
||||
standardPrice = stdHalf.price.toInt().toString(),
|
||||
proDesc = proHalf.originalPrice.takeIf { it > 0 }?.toInt()?.toString() ?: "",
|
||||
standardDesc = stdHalf.originalPrice.takeIf { it > 0 }?.toInt()?.toString() ?: "",
|
||||
id = "half_year",
|
||||
proGoodsId = proHalf.goodsId,
|
||||
standardGoodsId = stdHalf.goodsId,
|
||||
)
|
||||
)
|
||||
}
|
||||
val stdYear = standard.goods.firstOrNull { it.period == "year" }
|
||||
val proYear = pro.goods.firstOrNull { it.period == "year" }
|
||||
if (stdYear != null && proYear != null) {
|
||||
list.add(
|
||||
VipPriceModel(
|
||||
title = "每年",
|
||||
proPrice = proYear.price.toInt().toString(),
|
||||
standardPrice = stdYear.price.toInt().toString(),
|
||||
proDesc = proYear.originalPrice.takeIf { it > 0 }?.toInt()?.toString() ?: "",
|
||||
standardDesc = stdYear.originalPrice.takeIf { it > 0 }?.toInt()?.toString() ?: "",
|
||||
id = VipPriceModel.YEAR_ID,
|
||||
proGoodsId = proYear.goodsId,
|
||||
standardGoodsId = stdYear.goodsId,
|
||||
)
|
||||
)
|
||||
}
|
||||
return list
|
||||
}
|
||||
}
|
||||
|
||||
private fun JsonElement.asStringOrEmpty(): String {
|
||||
return try { if (isJsonPrimitive && asJsonPrimitive.isString) asString else "" } catch (_: Exception) { "" }
|
||||
}
|
||||
|
||||
private fun JsonElement.asBooleanOrNull(): Boolean? {
|
||||
return try { if (isJsonPrimitive && asJsonPrimitive.isBoolean) asBoolean else null } catch (_: Exception) { null }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user