feat(android-terminal): Phase A — core infrastructure
Foundation work for the Android tablet POS app. Adds the singletons that every screen needs (config persistence, network awareness, auth) without touching the existing screen stubs yet — Phases B–F will build on these. - gradle: bcrypt 0.10.2 pinned (used in Phase C for offline PIN verify) - LoyaltyApi: + GET /api/v1/store/loyalty/categories endpoint - ApiModels: + category_ids on StampRequest / PointsEarnRequest, + CategoryItem / CategoryListResponse - DeviceConfigRepository: DataStore wrapper for the paired-tablet state (api_url, auth_token, store_id/code/name, cached program/pins/categories JSON, per-seller language, resetDevice()) - AuthInterceptor: rewrites every request's host to the paired api_url and adds Bearer auth — Retrofit keeps a placeholder baseUrl since the real URL only exists post-pair - NetworkMonitor: Flow<Boolean> isOnline from ConnectivityManager - StaffPinRepository / CategoryRepository: cache-or-refresh pattern, Moshi-serialized to DataStore - AppModule: wires AuthInterceptor before the logging interceptor - strings.xml: ~50 strings × 4 locales (en authoritative; fr/de/lb translated, mirroring the loyalty backend's i18n approach) Verified by ./gradlew assembleDebug — clean build, only pre-existing warnings (Theme statusBarColor deprecation, Moshi-kapt deprecation). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -101,6 +101,9 @@ dependencies {
|
||||
// DataStore (device config persistence)
|
||||
implementation(libs.datastore.preferences)
|
||||
|
||||
// bcrypt (verify staff PIN hashes locally — pure Java, no native deps)
|
||||
implementation(libs.bcrypt)
|
||||
|
||||
// Testing
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.junit.android)
|
||||
|
||||
@@ -44,6 +44,10 @@ interface LoyaltyApi {
|
||||
@GET("api/v1/store/loyalty/pins")
|
||||
suspend fun listPins(): PinListResponse
|
||||
|
||||
// Categories (for the action sheets — multi-select pills)
|
||||
@GET("api/v1/store/loyalty/categories")
|
||||
suspend fun listCategories(): CategoryListResponse
|
||||
|
||||
// Auth
|
||||
@POST("api/v1/store/auth/login")
|
||||
suspend fun login(@Body request: LoginRequest): LoginResponse
|
||||
|
||||
@@ -111,6 +111,7 @@ data class StampRequest(
|
||||
val qr_code: String? = null,
|
||||
val card_number: String? = null,
|
||||
val staff_pin: String? = null,
|
||||
val category_ids: List<Int>? = null,
|
||||
val notes: String? = null,
|
||||
)
|
||||
|
||||
@@ -147,6 +148,7 @@ data class PointsEarnRequest(
|
||||
val purchase_amount_cents: Int,
|
||||
val order_reference: String? = null,
|
||||
val staff_pin: String? = null,
|
||||
val category_ids: List<Int>? = null,
|
||||
val notes: String? = null,
|
||||
)
|
||||
|
||||
@@ -187,3 +189,22 @@ data class PinItem(
|
||||
val is_active: Boolean,
|
||||
val is_locked: Boolean,
|
||||
)
|
||||
|
||||
// ── Categories ──────────────────────────────────────────────────────────
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CategoryListResponse(
|
||||
val categories: List<CategoryItem>,
|
||||
val total: Int,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CategoryItem(
|
||||
val id: Int,
|
||||
val store_id: Int,
|
||||
val name: String,
|
||||
/** Per-language overrides keyed by language code (en/fr/de/lb). Null = use `name`. */
|
||||
val name_translations: Map<String, String>? = null,
|
||||
val display_order: Int = 0,
|
||||
val is_active: Boolean = true,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package lu.rewardflow.terminal.data.network
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import lu.rewardflow.terminal.data.repository.DeviceConfigRepository
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Per-request rewrite that pins the host to the device's paired ``api_url``
|
||||
* and adds the long-lived ``Authorization: Bearer <auth_token>`` header.
|
||||
*
|
||||
* Retrofit is built with a placeholder baseUrl because the real URL only
|
||||
* exists after pairing — this interceptor is what makes every call actually
|
||||
* go to the merchant's chosen server. When the device hasn't been paired
|
||||
* yet, requests pass through unchanged and will fail at the network layer
|
||||
* (which is fine: the only flows running pre-pairing are the setup-screen
|
||||
* verification call, which fills in the URL itself before invoking the API).
|
||||
*
|
||||
* `runBlocking` is intentional: OkHttp interceptors run on the IO dispatcher
|
||||
* pool and DataStore reads are cheap. The alternative (a non-suspend cache)
|
||||
* is more code for negligible gain.
|
||||
*/
|
||||
@Singleton
|
||||
class AuthInterceptor @Inject constructor(
|
||||
private val configRepository: DeviceConfigRepository,
|
||||
) : Interceptor {
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val original = chain.request()
|
||||
val (apiUrl, authToken) = runBlocking {
|
||||
configRepository.currentApiUrl() to configRepository.currentAuthToken()
|
||||
}
|
||||
|
||||
val builder = original.newBuilder()
|
||||
|
||||
if (apiUrl != null) {
|
||||
val configuredHost = apiUrl.toHttpUrlOrNull()
|
||||
if (configuredHost != null) {
|
||||
val rewritten = original.url.newBuilder()
|
||||
.scheme(configuredHost.scheme)
|
||||
.host(configuredHost.host)
|
||||
.port(configuredHost.port)
|
||||
.build()
|
||||
builder.url(rewritten)
|
||||
}
|
||||
}
|
||||
|
||||
if (!authToken.isNullOrBlank()) {
|
||||
builder.header("Authorization", "Bearer $authToken")
|
||||
}
|
||||
|
||||
return chain.proceed(builder.build())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package lu.rewardflow.terminal.data.network
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Reactive view of "do we have a usable internet connection right now".
|
||||
*
|
||||
* The terminal screen reads this to show the offline badge and to gate
|
||||
* redemption (which can't be queued — points/stamps can be redeemed only
|
||||
* online, see Phase E of the implementation plan). The sync worker also
|
||||
* uses it indirectly via WorkManager network constraints.
|
||||
*/
|
||||
@Singleton
|
||||
class NetworkMonitor @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) {
|
||||
|
||||
private val connectivityManager =
|
||||
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
val isOnline: Flow<Boolean> = callbackFlow {
|
||||
val callback = object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
trySend(true)
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
trySend(currentlyOnline())
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(
|
||||
network: Network,
|
||||
capabilities: NetworkCapabilities,
|
||||
) {
|
||||
trySend(currentlyOnline())
|
||||
}
|
||||
}
|
||||
|
||||
val request = NetworkRequest.Builder()
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||
.build()
|
||||
|
||||
connectivityManager.registerNetworkCallback(request, callback)
|
||||
// Emit the current state so collectors don't sit waiting for the
|
||||
// first transition.
|
||||
trySend(currentlyOnline())
|
||||
|
||||
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.stateIn(scope, SharingStarted.Eagerly, initialValue = currentlyOnline())
|
||||
|
||||
private fun currentlyOnline(): Boolean {
|
||||
val active = connectivityManager.activeNetwork ?: return false
|
||||
val caps = connectivityManager.getNetworkCapabilities(active) ?: return false
|
||||
return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
|
||||
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package lu.rewardflow.terminal.data.repository
|
||||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import kotlinx.coroutines.flow.first
|
||||
import lu.rewardflow.terminal.data.api.LoyaltyApi
|
||||
import lu.rewardflow.terminal.data.model.CategoryItem
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Cached transaction categories for the store the device is paired to.
|
||||
*
|
||||
* The terminal action sheets (add stamp / earn points) show category pills
|
||||
* that the staff member multi-selects. Categories rarely change, so the
|
||||
* tablet caches the list and refreshes opportunistically (on app launch
|
||||
* and after any successful network round-trip).
|
||||
*/
|
||||
@Singleton
|
||||
class CategoryRepository @Inject constructor(
|
||||
private val api: LoyaltyApi,
|
||||
private val configRepository: DeviceConfigRepository,
|
||||
moshi: Moshi,
|
||||
) {
|
||||
|
||||
private val listAdapter: JsonAdapter<List<CategoryItem>> = moshi.adapter(
|
||||
Types.newParameterizedType(List::class.java, CategoryItem::class.java)
|
||||
)
|
||||
|
||||
suspend fun refresh(): List<CategoryItem> {
|
||||
val response = api.listCategories()
|
||||
val categories = response.categories.filter { it.is_active }
|
||||
configRepository.saveCategories(listAdapter.toJson(categories))
|
||||
return categories
|
||||
}
|
||||
|
||||
suspend fun cached(): List<CategoryItem> {
|
||||
val raw = configRepository.categoriesJson.first() ?: return emptyList()
|
||||
return runCatching { listAdapter.fromJson(raw) ?: emptyList() }
|
||||
.getOrDefault(emptyList())
|
||||
}
|
||||
|
||||
suspend fun listOrRefresh(): List<CategoryItem> {
|
||||
val cached = cached()
|
||||
return cached.ifEmpty { refresh() }
|
||||
}
|
||||
|
||||
/** Pick the right display label for a category given the staff member's
|
||||
* language preference, falling back to the canonical `name`. */
|
||||
fun localizedName(category: CategoryItem, languageCode: String): String =
|
||||
category.name_translations?.get(languageCode) ?: category.name
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package lu.rewardflow.terminal.data.repository
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Persisted device configuration.
|
||||
*
|
||||
* Once a tablet is paired (via QR scan on the setup screen) the merchant
|
||||
* server hands the device three pieces of data: an api_url, a store_code
|
||||
* and a long-lived auth_token. They live here for the entire lifetime of
|
||||
* the pairing — the token expires after 1 year and isn't refreshed.
|
||||
*
|
||||
* The cached *_json fields are populated after pairing so the PIN screen
|
||||
* and the terminal can render before the network round-trips on each
|
||||
* launch.
|
||||
*/
|
||||
@Singleton
|
||||
class DeviceConfigRepository @Inject constructor(
|
||||
private val dataStore: DataStore<Preferences>,
|
||||
) {
|
||||
|
||||
// ── Flows (suspend collectors)
|
||||
val apiUrl: Flow<String?> = dataStore.data.map { it[KEY_API_URL] }
|
||||
val authToken: Flow<String?> = dataStore.data.map { it[KEY_AUTH_TOKEN] }
|
||||
val storeId: Flow<Int?> = dataStore.data.map { it[KEY_STORE_ID] }
|
||||
val storeCode: Flow<String?> = dataStore.data.map { it[KEY_STORE_CODE] }
|
||||
val storeName: Flow<String?> = dataStore.data.map { it[KEY_STORE_NAME] }
|
||||
val isDeviceSetUp: Flow<Boolean> = dataStore.data.map { it[KEY_IS_SET_UP] ?: false }
|
||||
|
||||
val programJson: Flow<String?> = dataStore.data.map { it[KEY_PROGRAM_JSON] }
|
||||
val staffPinsJson: Flow<String?> = dataStore.data.map { it[KEY_STAFF_PINS_JSON] }
|
||||
val categoriesJson: Flow<String?> = dataStore.data.map { it[KEY_CATEGORIES_JSON] }
|
||||
|
||||
// ── One-shot reads (when the call site is suspending and just needs
|
||||
// the current value, e.g. an OkHttp interceptor or a repository fetch).
|
||||
suspend fun currentAuthToken(): String? = dataStore.data.first()[KEY_AUTH_TOKEN]
|
||||
suspend fun currentApiUrl(): String? = dataStore.data.first()[KEY_API_URL]
|
||||
suspend fun currentStoreId(): Int? = dataStore.data.first()[KEY_STORE_ID]
|
||||
suspend fun currentStoreCode(): String? = dataStore.data.first()[KEY_STORE_CODE]
|
||||
|
||||
// ── Pairing: persist setup payload + cached config in a single transaction.
|
||||
suspend fun savePairing(
|
||||
apiUrl: String,
|
||||
authToken: String,
|
||||
storeId: Int,
|
||||
storeCode: String,
|
||||
storeName: String?,
|
||||
) {
|
||||
dataStore.edit { prefs ->
|
||||
prefs[KEY_API_URL] = apiUrl
|
||||
prefs[KEY_AUTH_TOKEN] = authToken
|
||||
prefs[KEY_STORE_ID] = storeId
|
||||
prefs[KEY_STORE_CODE] = storeCode
|
||||
if (storeName != null) prefs[KEY_STORE_NAME] = storeName
|
||||
prefs[KEY_IS_SET_UP] = true
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun saveProgram(json: String) {
|
||||
dataStore.edit { it[KEY_PROGRAM_JSON] = json }
|
||||
}
|
||||
|
||||
suspend fun saveStaffPins(json: String) {
|
||||
dataStore.edit { it[KEY_STAFF_PINS_JSON] = json }
|
||||
}
|
||||
|
||||
suspend fun saveCategories(json: String) {
|
||||
dataStore.edit { it[KEY_CATEGORIES_JSON] = json }
|
||||
}
|
||||
|
||||
/** Per-seller language preference, keyed by staff PIN id. */
|
||||
fun sellerLanguage(pinId: Int): Flow<String?> =
|
||||
dataStore.data.map { it[sellerLanguageKey(pinId)] }
|
||||
|
||||
suspend fun saveSellerLanguage(pinId: Int, lang: String) {
|
||||
dataStore.edit { it[sellerLanguageKey(pinId)] = lang }
|
||||
}
|
||||
|
||||
/**
|
||||
* Wipe the device back to factory state. The merchant has to re-pair
|
||||
* after this — the auth_token is gone, so the tablet can't call any
|
||||
* authenticated endpoint until a new QR is scanned.
|
||||
*/
|
||||
suspend fun resetDevice() {
|
||||
dataStore.edit { it.clear() }
|
||||
}
|
||||
|
||||
private fun sellerLanguageKey(pinId: Int) =
|
||||
stringPreferencesKey("seller_language_$pinId")
|
||||
|
||||
companion object {
|
||||
private val KEY_API_URL = stringPreferencesKey("api_url")
|
||||
private val KEY_AUTH_TOKEN = stringPreferencesKey("auth_token")
|
||||
private val KEY_STORE_ID = intPreferencesKey("store_id")
|
||||
private val KEY_STORE_CODE = stringPreferencesKey("store_code")
|
||||
private val KEY_STORE_NAME = stringPreferencesKey("store_name")
|
||||
private val KEY_IS_SET_UP = booleanPreferencesKey("IS_SET_UP")
|
||||
private val KEY_PROGRAM_JSON = stringPreferencesKey("program_json")
|
||||
private val KEY_STAFF_PINS_JSON = stringPreferencesKey("staff_pins_json")
|
||||
private val KEY_CATEGORIES_JSON = stringPreferencesKey("categories_json")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package lu.rewardflow.terminal.data.repository
|
||||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import kotlinx.coroutines.flow.first
|
||||
import lu.rewardflow.terminal.data.api.LoyaltyApi
|
||||
import lu.rewardflow.terminal.data.model.PinItem
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Source of truth for staff PINs on the device.
|
||||
*
|
||||
* Refreshes from ``GET /api/v1/store/loyalty/pins`` and caches the
|
||||
* serialized list in DataStore so the PIN screen can render before the
|
||||
* network call completes (and stay usable for the few seconds where the
|
||||
* tablet has no signal between transactions).
|
||||
*
|
||||
* The PIN-entry verification flow itself is wired in Phase C — this
|
||||
* repository is just the cache primitive.
|
||||
*/
|
||||
@Singleton
|
||||
class StaffPinRepository @Inject constructor(
|
||||
private val api: LoyaltyApi,
|
||||
private val configRepository: DeviceConfigRepository,
|
||||
moshi: Moshi,
|
||||
) {
|
||||
|
||||
private val listAdapter: JsonAdapter<List<PinItem>> = moshi.adapter(
|
||||
Types.newParameterizedType(List::class.java, PinItem::class.java)
|
||||
)
|
||||
|
||||
/** Hit the server, persist the result, return the freshly fetched list. */
|
||||
suspend fun refresh(): List<PinItem> {
|
||||
val response = api.listPins()
|
||||
val pins = response.pins
|
||||
configRepository.saveStaffPins(listAdapter.toJson(pins))
|
||||
return pins
|
||||
}
|
||||
|
||||
/** Last cached list. Empty if the device has never synced. */
|
||||
suspend fun cached(): List<PinItem> {
|
||||
val raw = configRepository.staffPinsJson.first() ?: return emptyList()
|
||||
return runCatching { listAdapter.fromJson(raw) ?: emptyList() }
|
||||
.getOrDefault(emptyList())
|
||||
}
|
||||
|
||||
/** Cached if available, otherwise hit the network. */
|
||||
suspend fun listOrRefresh(): List<PinItem> {
|
||||
val cached = cached()
|
||||
return cached.ifEmpty { refresh() }
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import lu.rewardflow.terminal.BuildConfig
|
||||
import lu.rewardflow.terminal.data.api.LoyaltyApi
|
||||
import lu.rewardflow.terminal.data.db.AppDatabase
|
||||
import lu.rewardflow.terminal.data.db.dao.PendingTransactionDao
|
||||
import lu.rewardflow.terminal.data.network.AuthInterceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
@@ -32,11 +33,14 @@ object AppModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideOkHttpClient(): OkHttpClient {
|
||||
fun provideOkHttpClient(authInterceptor: AuthInterceptor): OkHttpClient {
|
||||
val builder = OkHttpClient.Builder()
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.writeTimeout(30, TimeUnit.SECONDS)
|
||||
// AuthInterceptor must run BEFORE logging so the rewritten URL
|
||||
// and the Authorization header show up in the log line.
|
||||
.addInterceptor(authInterceptor)
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
val logging = HttpLoggingInterceptor()
|
||||
@@ -50,6 +54,11 @@ object AppModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRetrofit(okHttpClient: OkHttpClient, moshi: Moshi): Retrofit {
|
||||
// Placeholder baseUrl: AuthInterceptor rewrites the host on every
|
||||
// request to the device's paired api_url. Retrofit just needs SOME
|
||||
// valid URL at construction. Keeping DEFAULT_API_URL here so dev
|
||||
// builds work out-of-the-box pointing at 10.0.2.2:8000 before the
|
||||
// device is paired.
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(BuildConfig.DEFAULT_API_URL + "/")
|
||||
.client(okHttpClient)
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
<resources>
|
||||
<string name="app_name">RewardFlow Terminal</string>
|
||||
|
||||
<!-- Setup screen -->
|
||||
<string name="setup_title">RewardFlow Terminal</string>
|
||||
<string name="setup_instruction">Scanne den Pairing-QR aus den Filialeinstellungen</string>
|
||||
<string name="setup_or_manual">oder manuell eingeben</string>
|
||||
<string name="setup_api_url">API-URL</string>
|
||||
<string name="setup_store_code">Filialcode</string>
|
||||
<string name="setup_auth_token">Auth-Token</string>
|
||||
<string name="setup_connect">Verbinden</string>
|
||||
<string name="setup_connecting">Verbindung…</string>
|
||||
<string name="setup_invalid_qr">Ungültiger Pairing-QR</string>
|
||||
<string name="setup_connection_failed">Verbindung zum Server nicht möglich</string>
|
||||
<string name="setup_camera_permission_required">Kamera-Berechtigung wird benötigt, um den Pairing-QR zu scannen</string>
|
||||
|
||||
<!-- PIN screen -->
|
||||
<string name="pin_select_staff">Wähle deinen Namen</string>
|
||||
<string name="pin_enter">PIN eingeben</string>
|
||||
<string name="pin_wrong">Falscher PIN</string>
|
||||
<string name="pin_locked">PIN gesperrt</string>
|
||||
<string name="pin_pending_sync">%1$d ausstehende Synchronisationen</string>
|
||||
<string name="pin_all_synced">Alles synchronisiert</string>
|
||||
<string name="pin_clear">Löschen</string>
|
||||
<string name="pin_backspace">Zurück</string>
|
||||
<string name="pin_no_staff">Kein Personal für diese Filiale konfiguriert</string>
|
||||
|
||||
<!-- Terminal screen -->
|
||||
<string name="terminal_search_hint">Kartennummer oder E-Mail</string>
|
||||
<string name="terminal_scan_qr">QR scannen</string>
|
||||
<string name="terminal_enroll_customer">Kunde anmelden</string>
|
||||
<string name="terminal_no_customer_title">Kein Kunde ausgewählt</string>
|
||||
<string name="terminal_no_customer_hint">QR scannen oder per Karte / E-Mail suchen</string>
|
||||
<string name="terminal_recent_transactions">Letzte Transaktionen</string>
|
||||
<string name="terminal_lock">Sperren</string>
|
||||
<string name="terminal_offline">Offline</string>
|
||||
<string name="terminal_online">Online</string>
|
||||
<string name="terminal_card_label">Karte</string>
|
||||
<string name="terminal_close_customer">Kunde abwählen</string>
|
||||
|
||||
<!-- Actions -->
|
||||
<string name="action_add_stamp">Stempel hinzufügen</string>
|
||||
<string name="action_earn_points">Punkte gutschreiben</string>
|
||||
<string name="action_redeem_stamps">Stempel einlösen</string>
|
||||
<string name="action_redeem_reward">Prämie einlösen</string>
|
||||
<string name="action_purchase_amount">Kaufbetrag (€)</string>
|
||||
<string name="action_select_category">Kategorie wählen</string>
|
||||
<string name="action_confirm">Bestätigen</string>
|
||||
<string name="action_cancel">Abbrechen</string>
|
||||
|
||||
<!-- Enrollment -->
|
||||
<string name="enroll_title">Neuen Kunden anmelden</string>
|
||||
<string name="enroll_name">Name</string>
|
||||
<string name="enroll_email">E-Mail</string>
|
||||
<string name="enroll_phone">Telefon (optional)</string>
|
||||
<string name="enroll_birthday">Geburtstag (optional)</string>
|
||||
<string name="enroll_submit">Anmelden</string>
|
||||
|
||||
<!-- Status / units -->
|
||||
<string name="balance_points">%1$d Punkte</string>
|
||||
<string name="balance_stamps">%1$d / %2$d Stempel</string>
|
||||
|
||||
<!-- Errors / toasts -->
|
||||
<string name="error_connection">Verbindung fehlgeschlagen</string>
|
||||
<string name="error_no_internet">Keine Internetverbindung</string>
|
||||
<string name="error_try_again">Erneut versuchen</string>
|
||||
<string name="error_card_not_found">Karte nicht gefunden</string>
|
||||
<string name="error_offline_redeem">Einlösen erfordert eine Internetverbindung</string>
|
||||
<string name="error_unknown">Etwas ist schiefgelaufen</string>
|
||||
|
||||
<!-- Generic -->
|
||||
<string name="generic_loading">Wird geladen…</string>
|
||||
<string name="generic_yes">Ja</string>
|
||||
<string name="generic_no">Nein</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,75 @@
|
||||
<resources>
|
||||
<string name="app_name">RewardFlow Terminal</string>
|
||||
|
||||
<!-- Setup screen -->
|
||||
<string name="setup_title">Terminal RewardFlow</string>
|
||||
<string name="setup_instruction">Scannez le QR d\'appairage depuis la page paramètres du magasin</string>
|
||||
<string name="setup_or_manual">ou saisie manuelle</string>
|
||||
<string name="setup_api_url">URL de l\'API</string>
|
||||
<string name="setup_store_code">Code magasin</string>
|
||||
<string name="setup_auth_token">Jeton d\'authentification</string>
|
||||
<string name="setup_connect">Connecter</string>
|
||||
<string name="setup_connecting">Connexion…</string>
|
||||
<string name="setup_invalid_qr">QR d\'appairage invalide</string>
|
||||
<string name="setup_connection_failed">Connexion au serveur impossible</string>
|
||||
<string name="setup_camera_permission_required">L\'accès à la caméra est requis pour scanner le QR d\'appairage</string>
|
||||
|
||||
<!-- PIN screen -->
|
||||
<string name="pin_select_staff">Sélectionnez votre nom</string>
|
||||
<string name="pin_enter">Saisissez votre PIN</string>
|
||||
<string name="pin_wrong">PIN incorrect</string>
|
||||
<string name="pin_locked">PIN verrouillé</string>
|
||||
<string name="pin_pending_sync">%1$d en attente de synchronisation</string>
|
||||
<string name="pin_all_synced">Tout est synchronisé</string>
|
||||
<string name="pin_clear">Effacer</string>
|
||||
<string name="pin_backspace">Retour</string>
|
||||
<string name="pin_no_staff">Aucun personnel configuré pour ce magasin</string>
|
||||
|
||||
<!-- Terminal screen -->
|
||||
<string name="terminal_search_hint">Numéro de carte ou e-mail</string>
|
||||
<string name="terminal_scan_qr">Scanner un QR</string>
|
||||
<string name="terminal_enroll_customer">Inscrire un client</string>
|
||||
<string name="terminal_no_customer_title">Aucun client sélectionné</string>
|
||||
<string name="terminal_no_customer_hint">Scannez un QR ou recherchez par carte / e-mail</string>
|
||||
<string name="terminal_recent_transactions">Transactions récentes</string>
|
||||
<string name="terminal_lock">Verrouiller</string>
|
||||
<string name="terminal_offline">Hors ligne</string>
|
||||
<string name="terminal_online">En ligne</string>
|
||||
<string name="terminal_card_label">Carte</string>
|
||||
<string name="terminal_close_customer">Désélectionner le client</string>
|
||||
|
||||
<!-- Actions -->
|
||||
<string name="action_add_stamp">Ajouter un tampon</string>
|
||||
<string name="action_earn_points">Gagner des points</string>
|
||||
<string name="action_redeem_stamps">Échanger les tampons</string>
|
||||
<string name="action_redeem_reward">Échanger une récompense</string>
|
||||
<string name="action_purchase_amount">Montant de l\'achat (€)</string>
|
||||
<string name="action_select_category">Choisir la catégorie</string>
|
||||
<string name="action_confirm">Confirmer</string>
|
||||
<string name="action_cancel">Annuler</string>
|
||||
|
||||
<!-- Enrollment -->
|
||||
<string name="enroll_title">Inscrire un nouveau client</string>
|
||||
<string name="enroll_name">Nom</string>
|
||||
<string name="enroll_email">E-mail</string>
|
||||
<string name="enroll_phone">Téléphone (facultatif)</string>
|
||||
<string name="enroll_birthday">Anniversaire (facultatif)</string>
|
||||
<string name="enroll_submit">Inscrire</string>
|
||||
|
||||
<!-- Status / units -->
|
||||
<string name="balance_points">%1$d points</string>
|
||||
<string name="balance_stamps">%1$d / %2$d tampons</string>
|
||||
|
||||
<!-- Errors / toasts -->
|
||||
<string name="error_connection">Échec de la connexion</string>
|
||||
<string name="error_no_internet">Aucune connexion internet</string>
|
||||
<string name="error_try_again">Réessayer</string>
|
||||
<string name="error_card_not_found">Carte introuvable</string>
|
||||
<string name="error_offline_redeem">L\'échange nécessite une connexion internet</string>
|
||||
<string name="error_unknown">Une erreur est survenue</string>
|
||||
|
||||
<!-- Generic -->
|
||||
<string name="generic_loading">Chargement…</string>
|
||||
<string name="generic_yes">Oui</string>
|
||||
<string name="generic_no">Non</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,75 @@
|
||||
<resources>
|
||||
<string name="app_name">RewardFlow Terminal</string>
|
||||
|
||||
<!-- Setup screen -->
|
||||
<string name="setup_title">RewardFlow Terminal</string>
|
||||
<string name="setup_instruction">Scannt de Pairing-QR aus de Geschäftsastellungen</string>
|
||||
<string name="setup_or_manual">oder manuell aginn</string>
|
||||
<string name="setup_api_url">API-URL</string>
|
||||
<string name="setup_store_code">Geschäftscode</string>
|
||||
<string name="setup_auth_token">Auth-Token</string>
|
||||
<string name="setup_connect">Verbannen</string>
|
||||
<string name="setup_connecting">Verbindung…</string>
|
||||
<string name="setup_invalid_qr">Ongëltege Pairing-QR</string>
|
||||
<string name="setup_connection_failed">Server-Verbindung net méiglech</string>
|
||||
<string name="setup_camera_permission_required">Camera-Erlabnis gëtt gebraucht fir de Pairing-QR ze scannen</string>
|
||||
|
||||
<!-- PIN screen -->
|
||||
<string name="pin_select_staff">Wielt Ären Numm</string>
|
||||
<string name="pin_enter">PIN aginn</string>
|
||||
<string name="pin_wrong">Falsche PIN</string>
|
||||
<string name="pin_locked">PIN gespaart</string>
|
||||
<string name="pin_pending_sync">%1$d an der Sync-Schlaang</string>
|
||||
<string name="pin_all_synced">Alles synchroniséiert</string>
|
||||
<string name="pin_clear">Läschen</string>
|
||||
<string name="pin_backspace">Zréck</string>
|
||||
<string name="pin_no_staff">Keng Mataarbechter fir dëst Geschäft konfiguréiert</string>
|
||||
|
||||
<!-- Terminal screen -->
|
||||
<string name="terminal_search_hint">Kaartennummer oder E-Mail</string>
|
||||
<string name="terminal_scan_qr">QR scannen</string>
|
||||
<string name="terminal_enroll_customer">Client umellen</string>
|
||||
<string name="terminal_no_customer_title">Kee Client gewielt</string>
|
||||
<string name="terminal_no_customer_hint">Scannt e QR oder sicht no Kaart / E-Mail</string>
|
||||
<string name="terminal_recent_transactions">Lescht Transaktiounen</string>
|
||||
<string name="terminal_lock">Spären</string>
|
||||
<string name="terminal_offline">Offline</string>
|
||||
<string name="terminal_online">Online</string>
|
||||
<string name="terminal_card_label">Kaart</string>
|
||||
<string name="terminal_close_customer">Client ofwielen</string>
|
||||
|
||||
<!-- Actions -->
|
||||
<string name="action_add_stamp">Stempel derbäisetzen</string>
|
||||
<string name="action_earn_points">Punkten verdéngen</string>
|
||||
<string name="action_redeem_stamps">Stempel anléisen</string>
|
||||
<string name="action_redeem_reward">Prime anléisen</string>
|
||||
<string name="action_purchase_amount">Akafsbetrag (€)</string>
|
||||
<string name="action_select_category">Kategorie wielen</string>
|
||||
<string name="action_confirm">Bestätegen</string>
|
||||
<string name="action_cancel">Ofbriechen</string>
|
||||
|
||||
<!-- Enrollment -->
|
||||
<string name="enroll_title">Neie Client umellen</string>
|
||||
<string name="enroll_name">Numm</string>
|
||||
<string name="enroll_email">E-Mail</string>
|
||||
<string name="enroll_phone">Telefon (fakultativ)</string>
|
||||
<string name="enroll_birthday">Gebuertsdag (fakultativ)</string>
|
||||
<string name="enroll_submit">Umellen</string>
|
||||
|
||||
<!-- Status / units -->
|
||||
<string name="balance_points">%1$d Punkten</string>
|
||||
<string name="balance_stamps">%1$d / %2$d Stempel</string>
|
||||
|
||||
<!-- Errors / toasts -->
|
||||
<string name="error_connection">Verbindung huet net geklappt</string>
|
||||
<string name="error_no_internet">Keng Internet-Verbindung</string>
|
||||
<string name="error_try_again">Nees probéieren</string>
|
||||
<string name="error_card_not_found">Kaart net fonnt</string>
|
||||
<string name="error_offline_redeem">Anléise brauch eng Internet-Verbindung</string>
|
||||
<string name="error_unknown">Eppes ass schif gaangen</string>
|
||||
|
||||
<!-- Generic -->
|
||||
<string name="generic_loading">Lueden…</string>
|
||||
<string name="generic_yes">Jo</string>
|
||||
<string name="generic_no">Nee</string>
|
||||
</resources>
|
||||
@@ -1,3 +1,75 @@
|
||||
<resources>
|
||||
<string name="app_name">RewardFlow Terminal</string>
|
||||
|
||||
<!-- Setup screen -->
|
||||
<string name="setup_title">RewardFlow Terminal</string>
|
||||
<string name="setup_instruction">Scan the setup QR from your store settings page</string>
|
||||
<string name="setup_or_manual">or enter manually</string>
|
||||
<string name="setup_api_url">API URL</string>
|
||||
<string name="setup_store_code">Store Code</string>
|
||||
<string name="setup_auth_token">Auth Token</string>
|
||||
<string name="setup_connect">Connect</string>
|
||||
<string name="setup_connecting">Connecting…</string>
|
||||
<string name="setup_invalid_qr">Invalid pairing QR code</string>
|
||||
<string name="setup_connection_failed">Could not connect to server</string>
|
||||
<string name="setup_camera_permission_required">Camera permission is required to scan the pairing QR</string>
|
||||
|
||||
<!-- PIN screen -->
|
||||
<string name="pin_select_staff">Select your name</string>
|
||||
<string name="pin_enter">Enter your PIN</string>
|
||||
<string name="pin_wrong">Wrong PIN</string>
|
||||
<string name="pin_locked">PIN locked</string>
|
||||
<string name="pin_pending_sync">%1$d pending sync</string>
|
||||
<string name="pin_all_synced">All synced</string>
|
||||
<string name="pin_clear">Clear</string>
|
||||
<string name="pin_backspace">Backspace</string>
|
||||
<string name="pin_no_staff">No staff configured for this store</string>
|
||||
|
||||
<!-- Terminal screen -->
|
||||
<string name="terminal_search_hint">Card number or email</string>
|
||||
<string name="terminal_scan_qr">Scan QR Code</string>
|
||||
<string name="terminal_enroll_customer">Enroll Customer</string>
|
||||
<string name="terminal_no_customer_title">No customer selected</string>
|
||||
<string name="terminal_no_customer_hint">Scan a QR or search by card / email</string>
|
||||
<string name="terminal_recent_transactions">Recent Transactions</string>
|
||||
<string name="terminal_lock">Lock</string>
|
||||
<string name="terminal_offline">Offline</string>
|
||||
<string name="terminal_online">Online</string>
|
||||
<string name="terminal_card_label">Card</string>
|
||||
<string name="terminal_close_customer">Clear customer</string>
|
||||
|
||||
<!-- Actions -->
|
||||
<string name="action_add_stamp">Add Stamp</string>
|
||||
<string name="action_earn_points">Earn Points</string>
|
||||
<string name="action_redeem_stamps">Redeem Stamps</string>
|
||||
<string name="action_redeem_reward">Redeem Reward</string>
|
||||
<string name="action_purchase_amount">Purchase amount (€)</string>
|
||||
<string name="action_select_category">Select category</string>
|
||||
<string name="action_confirm">Confirm</string>
|
||||
<string name="action_cancel">Cancel</string>
|
||||
|
||||
<!-- Enrollment -->
|
||||
<string name="enroll_title">Enroll New Customer</string>
|
||||
<string name="enroll_name">Name</string>
|
||||
<string name="enroll_email">Email</string>
|
||||
<string name="enroll_phone">Phone (optional)</string>
|
||||
<string name="enroll_birthday">Birthday (optional)</string>
|
||||
<string name="enroll_submit">Enroll</string>
|
||||
|
||||
<!-- Status / units -->
|
||||
<string name="balance_points">%1$d Points</string>
|
||||
<string name="balance_stamps">%1$d / %2$d Stamps</string>
|
||||
|
||||
<!-- Errors / toasts -->
|
||||
<string name="error_connection">Connection failed</string>
|
||||
<string name="error_no_internet">No internet</string>
|
||||
<string name="error_try_again">Try again</string>
|
||||
<string name="error_card_not_found">Card not found</string>
|
||||
<string name="error_offline_redeem">Redemption requires an internet connection</string>
|
||||
<string name="error_unknown">Something went wrong</string>
|
||||
|
||||
<!-- Generic -->
|
||||
<string name="generic_loading">Loading…</string>
|
||||
<string name="generic_yes">Yes</string>
|
||||
<string name="generic_no">No</string>
|
||||
</resources>
|
||||
|
||||
@@ -44,6 +44,9 @@ mlkitBarcode = "17.3.0"
|
||||
# DataStore (preferences)
|
||||
datastore = "1.1.2"
|
||||
|
||||
# bcrypt — pure-Java, used to verify staff PIN hashes locally on the tablet
|
||||
bcrypt = "0.10.2"
|
||||
|
||||
# Testing
|
||||
junit = "4.13.2"
|
||||
junitAndroid = "1.2.1"
|
||||
@@ -100,6 +103,9 @@ mlkit-barcode = { group = "com.google.mlkit", name = "barcode-scanning", version
|
||||
# DataStore
|
||||
datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
|
||||
|
||||
# bcrypt
|
||||
bcrypt = { group = "at.favre.lib", name = "bcrypt", version.ref = "bcrypt" }
|
||||
|
||||
# Testing
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
junit-android = { group = "androidx.test.ext", name = "junit", version.ref = "junitAndroid" }
|
||||
|
||||
Reference in New Issue
Block a user