Skip to content
Open
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
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,8 @@ android {
applicationId = "com.pinakes.app"
minSdk = 26
targetSdk = 35
versionCode = 7
versionName = "1.3.1"
versionCode = 8
versionName = "1.4.0"

vectorDrawables {
useSupportLibrary = true
Expand Down
66 changes: 66 additions & 0 deletions app/src/main/java/com/pinakes/app/data/model/Models.kt
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,55 @@ data class RegisterRequest(
val nome: String,
val cognome: String,
val email: String,
// telefono/indirizzo stay non-null but the screen may send "" when the instance
// marks them optional (their required-ness is driven by the discovery schema).
val telefono: String,
val indirizzo: String,
val password: String, // min 8, max 72
@SerialName("password_confirm") val passwordConfirm: String,
@SerialName("privacy_acceptance") val privacyAcceptance: Boolean,
// Instance-defined custom registration fields, keyed by the field id (as a string) →
// scalar value. checkbox → "1"/"". Omitted when the instance defines none (explicitNulls
// = false drops the null). See [RegistrationFieldsPayload.customFields].
@SerialName("custom_fields") val customFields: Map<String, String>? = null,
)

// ---------- Registration discovery ----------
// Mirrors GET /auth/registration-fields (public, no token). Tells the client which built-in
// fields the instance requires and what extra custom fields to render on the sign-up form.
@Serializable
data class RegistrationFieldsPayload(
@SerialName("registration_enabled") val registrationEnabled: Boolean = false,
// Config-driven built-ins only: keys are "cognome" | "telefono" | "indirizzo".
@SerialName("builtin_fields") val builtinFields: Map<String, BuiltinFieldRule> = emptyMap(),
@SerialName("custom_fields") val customFields: List<CustomFieldDef> = emptyList(),
)

@Serializable
data class BuiltinFieldRule(
val required: Boolean = false,
val configurable: Boolean = false,
)

// One active custom field definition (matches RegistrationFields::apiDefinitions()). `options`
// is tolerated for forward-compat only — the current server emits neither it nor a select type.
@Serializable
data class CustomFieldDef(
val id: Int = 0,
val label: String = "",
val type: String = "text", // text | textarea | email | url | number | checkbox
val required: Boolean = false,
val options: List<String> = emptyList(),
)

// A custom field with the current user's value (matches editableFieldsForUser() in GET /me).
@Serializable
data class CustomFieldValue(
val id: Int = 0,
val label: String = "",
val type: String = "text", // text | textarea | email | url | number | checkbox
val required: Boolean = false,
val value: String = "",
)

@Serializable
Expand All @@ -97,6 +141,19 @@ data class UserProfile(
@SerialName("email_verificata") val emailVerificata: Boolean = false,
val stato: String? = null,
@SerialName("avatar_url") val avatarUrl: String? = null,
// Editable profile fields GET /me returns (previously dropped by the model).
val telefono: String? = null,
val indirizzo: String? = null,
@SerialName("data_nascita") val dataNascita: String? = null, // "yyyy-MM-dd"
val sesso: String? = null,
@SerialName("cod_fiscale") val codFiscale: String? = null,
// Read-only membership fields.
@SerialName("codice_tessera") val codiceTessera: String? = null,
@SerialName("card_expires_at") val cardExpiresAt: String? = null,
@SerialName("last_access_at") val lastAccessAt: String? = null,
val locale: String? = null,
// Instance-defined custom fields with the user's current values.
@SerialName("custom_fields") val customFields: List<CustomFieldValue> = emptyList(),
) {
val fullName: String get() = listOf(nome, cognome).filter { it.isNotBlank() }.joinToString(" ")
}
Expand All @@ -105,6 +162,15 @@ data class UserProfile(
data class UpdateProfileRequest(
val nome: String? = null,
val cognome: String? = null,
val telefono: String? = null,
val indirizzo: String? = null,
@SerialName("data_nascita") val dataNascita: String? = null, // "yyyy-MM-dd"
@SerialName("cod_fiscale") val codFiscale: String? = null,
val sesso: String? = null,
val locale: String? = null,
// Only sent ids change; "" clears a field. Null → omitted (explicitNulls = false) so PATCH
// stays partial.
@SerialName("custom_fields") val customFields: Map<String, String>? = null,
)

@Serializable
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/java/com/pinakes/app/data/network/PinakesApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import com.pinakes.app.data.model.ReviewRequest
import com.pinakes.app.data.model.PushPrefs
import com.pinakes.app.data.model.PushSubscribeRequest
import com.pinakes.app.data.model.RegisterRequest
import com.pinakes.app.data.model.RegistrationFieldsPayload
import com.pinakes.app.data.model.ReservationItem
import com.pinakes.app.data.model.ReservationRequest
import com.pinakes.app.data.model.UpdateProfileRequest
Expand Down Expand Up @@ -62,6 +63,11 @@ interface PinakesApi {
@Headers(NO_AUTH)
suspend fun register(@Body body: RegisterRequest): Envelope<Unit>

/** Public discovery of the sign-up form: which built-ins are required + custom fields. */
@GET("auth/registration-fields")
@Headers(NO_AUTH)
suspend fun registrationFields(): Envelope<RegistrationFieldsPayload>

@POST("auth/forgot-password")
@Headers(NO_AUTH)
suspend fun forgotPassword(@Body body: ForgotRequest): Envelope<Unit>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.pinakes.app.data.model.ForgotRequest
import com.pinakes.app.data.model.HealthPayload
import com.pinakes.app.data.model.LoginRequest
import com.pinakes.app.data.model.RegisterRequest
import com.pinakes.app.data.model.RegistrationFieldsPayload
import com.pinakes.app.data.network.ApiResult
import com.pinakes.app.data.network.NetworkModule
import com.pinakes.app.data.network.apiCall
Expand Down Expand Up @@ -121,6 +122,15 @@ class AuthRepository(
}
}

/**
* Public discovery of the sign-up form schema (built-in required flags + custom fields) for
* the committed instance. No token required.
*/
suspend fun registrationFields(): ApiResult<RegistrationFieldsPayload> {
val api = network.api()
return apiCall { api.registrationFields() }
}

suspend fun register(
nome: String,
cognome: String,
Expand All @@ -130,6 +140,7 @@ class AuthRepository(
password: String,
passwordConfirm: String,
privacyAccepted: Boolean,
customFields: Map<String, String>? = null,
): ApiResult<Unit> {
val api = network.api()
return apiCall {
Expand All @@ -143,6 +154,7 @@ class AuthRepository(
password = password,
passwordConfirm = passwordConfirm,
privacyAcceptance = privacyAccepted,
customFields = customFields?.takeIf { it.isNotEmpty() },
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,15 @@ class ProfileRepository(private val network: NetworkModule) {
return apiCall { api.me() }
}

suspend fun updateProfile(nome: String?, cognome: String?): ApiResult<UserProfile> {
/**
* PATCH /me with only the changed fields. Null fields are omitted (partial PATCH via
* explicitNulls = false); a custom field mapped to "" clears that one field.
*/
suspend fun updateProfile(request: UpdateProfileRequest): ApiResult<UserProfile> {
val api = network.api()
// PATCH /me returns null/UserProfile. Model it as Unit so apiCall never tries to cast a
// null body to UserProfile, then re-fetch the canonical profile on success.
return when (val res = apiCall { api.updateMe(UpdateProfileRequest(nome, cognome)).asUnit() }) {
return when (val res = apiCall { api.updateMe(request).asUnit() }) {
is ApiResult.Success -> me()
is ApiResult.Failure -> res
}
Expand Down
Loading
Loading