Skip to content
Merged
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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ All notable changes to this project will be documented in this file.

### Added
- Implemented pagination for item tags list screen.
- Implemented CodedError system with NATA-XXXX error codes.
- Implemented CodedError system with NATIVEAPPTEMPLATE-XXXX error codes.
- Added unit tests for utils, network, and pre-push hook.
- Added Spotless + ktlint for Kotlin code formatting.
- Added app version and reorganized settings sections.
Expand Down
12 changes: 6 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ MVVM layered architecture following [Android Modern App Architecture](https://de
- **Network Layer** (`network/`): `AuthInterceptor` for token injection, `RequestHelper` for request construction.
- **DI** (`di/modules/`): Hilt modules — `NetModule` (Retrofit/OkHttp), `DataModule` (repository bindings), `DataStoreModule`, `DispatchersModule`, `CoroutineScopesModule`.
- **DataStore** (`datastore/`): Proto DataStore for user preferences. Proto definitions live in `app/src/main/proto/`.
- **Navigation**: `NatNavHost.kt` is the top-level nav graph. Three bottom-nav sections: Shops, Scan, Settings. Each section uses nested navigation graphs via `*BaseRoute`.
- **Navigation**: `NativeAppTemplateNavHost.kt` is the top-level nav graph. Three bottom-nav sections: Shops, Scan, Settings. Each section uses nested navigation graphs via `*BaseRoute`.

## Key Patterns

Expand All @@ -49,15 +49,15 @@ MVVM layered architecture following [Android Modern App Architecture](https://de
- **Proto DataStore**: User preferences and NFC scan state are persisted via Protocol Buffers (lite).

## Error Handling (CodedError System)
All errors should use the `CodedError` interface. Error codes use the `NATA-XXXX` prefix (NativeAppTemplate Android).
All errors should use the `CodedError` interface. Error codes use the `NATIVEAPPTEMPLATE-XXXX` prefix (NativeAppTemplate Android).

| Range | Type | Description |
|-------|------|-------------|
| NATA-1xxx | App/general errors | Unexpected errors, catch-all |
| NATA-2xxx | API/network errors | HTTP request failures, parsing errors |
| NATIVEAPPTEMPLATE-1xxx | App/general errors | Unexpected errors, catch-all |
| NATIVEAPPTEMPLATE-2xxx | API/network errors | HTTP request failures, parsing errors |

- New error types must implement `CodedError`
- Use `codedDescription` (not `message` or `localizedMessage`) in all user-facing error messages — this prepends `[NATA-XXXX]` for `CodedError` types
- Use `codedDescription` (not `message` or `localizedMessage`) in all user-facing error messages — this prepends `[NATIVEAPPTEMPLATE-XXXX]` for `CodedError` types

## Testing

Expand All @@ -76,4 +76,4 @@ cp scripts/pre-push .git/hooks/pre-push

## Connecting to Local API

The debug `buildConfigField` entries in `app/build.gradle.kts` read `NATEMPLATE_API_DOMAIN`, `NATEMPLATE_API_PORT`, and `NATEMPLATE_API_SCHEME` via `project.findProperty(...)` (not `System.getenv` — Android Studio launched from Finder/Dock does not inherit shell env). Set them in `~/.gradle/gradle.properties` (user-global, per-developer); the same config then works from both the terminal and the IDE. Falls back to `https://api.nativeapptemplate.com` when unset. One-off override: `./gradlew -PNATEMPLATE_API_DOMAIN=... assembleDebug`.
The debug `buildConfigField` entries in `app/build.gradle.kts` read `NATIVEAPPTEMPLATE_API_DOMAIN`, `NATIVEAPPTEMPLATE_API_PORT`, and `NATIVEAPPTEMPLATE_API_SCHEME` via `project.findProperty(...)` (not `System.getenv` — Android Studio launched from Finder/Dock does not inherit shell env). Set them in `~/.gradle/gradle.properties` (user-global, per-developer); the same config then works from both the terminal and the IDE. Falls back to `https://api.nativeapptemplate.com` when unset. One-off override: `./gradlew -PNATIVEAPPTEMPLATE_API_DOMAIN=... assembleDebug`.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,12 @@ By default the debug build hits the hosted API (`https://api.nativeapptemplate.c
```
# Use your current Wi-Fi IP (macOS: `ipconfig getifaddr en0`), or 10.0.2.2 for emulator → host.
# Never use 127.0.0.1, localhost, or 0.0.0.0 — Rails and this app must agree on one reachable address.
NATEMPLATE_API_DOMAIN=192.168.1.21
NATEMPLATE_API_PORT=3000
NATEMPLATE_API_SCHEME=http
NATIVEAPPTEMPLATE_API_DOMAIN=192.168.1.21
NATIVEAPPTEMPLATE_API_PORT=3000
NATIVEAPPTEMPLATE_API_SCHEME=http
```

Then `./gradlew assembleDebug` — or Build → Rebuild Project from Android Studio. The debug `buildConfigField` entries in `app/build.gradle.kts` read these via `project.findProperty(...)`, so the same config works from both the terminal and the IDE. Remove the three properties to fall back to the hosted default. For a one-off override: `./gradlew -PNATEMPLATE_API_DOMAIN=192.168.1.21 -PNATEMPLATE_API_PORT=3000 -PNATEMPLATE_API_SCHEME=http assembleDebug`.
Then `./gradlew assembleDebug` — or Build → Rebuild Project from Android Studio. The debug `buildConfigField` entries in `app/build.gradle.kts` read these via `project.findProperty(...)`, so the same config works from both the terminal and the IDE. Remove the three properties to fall back to the hosted default. For a one-off override: `./gradlew -PNATIVEAPPTEMPLATE_API_DOMAIN=192.168.1.21 -PNATIVEAPPTEMPLATE_API_PORT=3000 -PNATIVEAPPTEMPLATE_API_SCHEME=http assembleDebug`.

Cleartext HTTP to private IPs is already permitted in debug via `app/src/debug/res/xml/network_security_config.xml`; the release config (in `app/src/main/`) keeps `api.nativeapptemplate.com` HTTPS-only.

Expand Down
6 changes: 3 additions & 3 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ android {
debug {
extra["alwaysUpdateBuildId"] = false
isDebuggable = true
buildConfigField("String", "DOMAIN", "\"${(project.findProperty("NATEMPLATE_API_DOMAIN") as String?)?.trim() ?: "api.nativeapptemplate.com"}\"")
buildConfigField("String", "PORT", "\"${(project.findProperty("NATEMPLATE_API_PORT") as String?)?.trim() ?: ""}\"")
buildConfigField("String", "SCHEME", "\"${(project.findProperty("NATEMPLATE_API_SCHEME") as String?)?.trim() ?: "https"}\"")
buildConfigField("String", "DOMAIN", "\"${(project.findProperty("NATIVEAPPTEMPLATE_API_DOMAIN") as String?)?.trim() ?: "api.nativeapptemplate.com"}\"")
buildConfigField("String", "PORT", "\"${(project.findProperty("NATIVEAPPTEMPLATE_API_PORT") as String?)?.trim() ?: ""}\"")
buildConfigField("String", "SCHEME", "\"${(project.findProperty("NATIVEAPPTEMPLATE_API_SCHEME") as String?)?.trim() ?: "https"}\"")
}

release {
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.Nat.Splash"
android:theme="@style/Theme.NativeAppTemplate.Splash"
android:networkSecurityConfig="@xml/network_security_config"
tools:ignore="GoogleAppIndexingWarning"
tools:targetApi="tiramisu">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ import androidx.lifecycle.repeatOnLifecycle
import com.nativeapptemplate.nativeapptemplatefree.MainActivityUiState.Loading
import com.nativeapptemplate.nativeapptemplatefree.MainActivityUiState.Success
import com.nativeapptemplate.nativeapptemplatefree.data.login.LoginRepository
import com.nativeapptemplate.nativeapptemplatefree.designsystem.theme.NatTheme
import com.nativeapptemplate.nativeapptemplatefree.designsystem.theme.NativeAppTemplateTheme
import com.nativeapptemplate.nativeapptemplatefree.model.DarkThemeConfig
import com.nativeapptemplate.nativeapptemplatefree.ui.app_root.NatApp
import com.nativeapptemplate.nativeapptemplatefree.ui.app_root.rememberNatAppState
import com.nativeapptemplate.nativeapptemplatefree.ui.app_root.NativeAppTemplateApp
import com.nativeapptemplate.nativeapptemplatefree.ui.app_root.rememberNativeAppTemplateAppState
import com.nativeapptemplate.nativeapptemplatefree.utils.NetworkMonitor
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -89,15 +89,15 @@ class MainActivity : ComponentActivity() {
onDispose {}
}

val appState = rememberNatAppState(
val appState = rememberNativeAppTemplateAppState(
loginRepository = loginRepository,
networkMonitor = networkMonitor,
)

NatTheme(
NativeAppTemplateTheme(
darkTheme = darkTheme,
) {
NatApp(appState)
NativeAppTemplateApp(appState)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.nativeapptemplate.nativeapptemplatefree

object NatConstants {
object NativeAppTemplateConstants {
const val SUPPORT_MAIL: String = "support@nativeapptemplate.com"
const val SUPPORT_WEBSITE_URL: String = "https://nativeapptemplate.com"
const val FAQS_URL: String = "https://nativeapptemplate.com/faqs"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ sealed class ApiException(message: String, cause: Throwable? = null) :
val code: Int,
val apiMessage: String,
) : ApiException("$apiMessage [Status: $code]") {
override val errorCode: String = "NATA-2001"
override val errorCode: String = "NATIVEAPPTEMPLATE-2001"
override val errorDescription: String = "$apiMessage [Status: $code]"
}

class UnprocessableError(
val rawMessage: String,
cause: Throwable? = null,
) : ApiException("Not processable error($rawMessage).", cause) {
override val errorCode: String = "NATA-2002"
override val errorCode: String = "NATIVEAPPTEMPLATE-2002"
override val errorDescription: String = "Processing error: $rawMessage"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ sealed class AppError(
) : Exception(errorDescription), CodedError {

class Unexpected(detail: String? = null) : AppError(
errorCode = "NATA-1001",
errorCode = "NATIVEAPPTEMPLATE-1001",
errorDescription = "Unexpected error" + if (detail != null) ": $detail" else "",
)
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.nativeapptemplate.nativeapptemplatefree.data.item_tag

import com.nativeapptemplate.nativeapptemplatefree.datastore.NatPreferencesDataSource
import com.nativeapptemplate.nativeapptemplatefree.datastore.NativeAppTemplatePreferencesDataSource
import com.nativeapptemplate.nativeapptemplatefree.model.*
import com.nativeapptemplate.nativeapptemplatefree.network.Dispatcher
import com.nativeapptemplate.nativeapptemplatefree.network.NatDispatchers
import com.nativeapptemplate.nativeapptemplatefree.network.NativeAppTemplateDispatchers
import com.nativeapptemplate.nativeapptemplatefree.network.emitApiResponse
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.first
Expand All @@ -12,9 +12,9 @@ import kotlinx.coroutines.flow.flowOn
import javax.inject.Inject

class ItemTagRepositoryImpl @Inject constructor(
private val mtcPreferencesDataSource: NatPreferencesDataSource,
private val mtcPreferencesDataSource: NativeAppTemplatePreferencesDataSource,
private val api: ItemTagApi,
@Dispatcher(NatDispatchers.IO) private val ioDispatcher: CoroutineDispatcher,
@Dispatcher(NativeAppTemplateDispatchers.IO) private val ioDispatcher: CoroutineDispatcher,
) : ItemTagRepository {

override fun getItemTags(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.nativeapptemplate.nativeapptemplatefree.data.login

import com.nativeapptemplate.nativeapptemplatefree.datastore.NatPreferencesDataSource
import com.nativeapptemplate.nativeapptemplatefree.datastore.NativeAppTemplatePreferencesDataSource
import com.nativeapptemplate.nativeapptemplatefree.model.*
import com.nativeapptemplate.nativeapptemplatefree.network.Dispatcher
import com.nativeapptemplate.nativeapptemplatefree.network.NatDispatchers
import com.nativeapptemplate.nativeapptemplatefree.network.NativeAppTemplateDispatchers
import com.nativeapptemplate.nativeapptemplatefree.network.emitApiResponse
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.first
Expand All @@ -12,9 +12,9 @@ import kotlinx.coroutines.flow.flowOn
import javax.inject.Inject

class AccountPasswordRepositoryImpl @Inject constructor(
private val natPreferencesDataSource: NatPreferencesDataSource,
private val natPreferencesDataSource: NativeAppTemplatePreferencesDataSource,
private val api: AccountPasswordApi,
@Dispatcher(NatDispatchers.IO) private val ioDispatcher: CoroutineDispatcher,
@Dispatcher(NativeAppTemplateDispatchers.IO) private val ioDispatcher: CoroutineDispatcher,
) : AccountPasswordRepository {
override fun updateAccountPassword(
updatePasswordBody: UpdatePasswordBody,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ package com.nativeapptemplate.nativeapptemplatefree.data.login

import androidx.annotation.VisibleForTesting
import com.nativeapptemplate.nativeapptemplatefree.common.errors.ApiException
import com.nativeapptemplate.nativeapptemplatefree.datastore.NatPreferencesDataSource
import com.nativeapptemplate.nativeapptemplatefree.datastore.NativeAppTemplatePreferencesDataSource
import com.nativeapptemplate.nativeapptemplatefree.model.*
import com.nativeapptemplate.nativeapptemplatefree.model.LoggedInShopkeeper
import com.nativeapptemplate.nativeapptemplatefree.model.Login
import com.nativeapptemplate.nativeapptemplatefree.network.Dispatcher
import com.nativeapptemplate.nativeapptemplatefree.network.NatDispatchers
import com.nativeapptemplate.nativeapptemplatefree.network.NativeAppTemplateDispatchers
import com.nativeapptemplate.nativeapptemplatefree.network.emitApiResponse
import com.skydoves.sandwich.message
import com.skydoves.sandwich.suspendOnFailure
Expand All @@ -25,8 +25,8 @@ import javax.inject.Inject
@VisibleForTesting
class LoginRepositoryImpl @Inject constructor(
private val api: LoginApi,
private val natPreferencesDataSource: NatPreferencesDataSource,
@Dispatcher(NatDispatchers.IO) private val ioDispatcher: CoroutineDispatcher,
private val natPreferencesDataSource: NativeAppTemplatePreferencesDataSource,
@Dispatcher(NativeAppTemplateDispatchers.IO) private val ioDispatcher: CoroutineDispatcher,
) : LoginRepository {

override fun login(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import com.nativeapptemplate.nativeapptemplatefree.model.SignUp
import com.nativeapptemplate.nativeapptemplatefree.model.SignUpForUpdate
import com.nativeapptemplate.nativeapptemplatefree.model.Status
import com.nativeapptemplate.nativeapptemplatefree.network.Dispatcher
import com.nativeapptemplate.nativeapptemplatefree.network.NatDispatchers
import com.nativeapptemplate.nativeapptemplatefree.network.NativeAppTemplateDispatchers
import com.nativeapptemplate.nativeapptemplatefree.network.emitApiResponse
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.flow
Expand All @@ -15,7 +15,7 @@ import javax.inject.Inject

class SignUpRepositoryImpl @Inject constructor(
private val api: SignUpApi,
@Dispatcher(NatDispatchers.IO) private val ioDispatcher: CoroutineDispatcher,
@Dispatcher(NativeAppTemplateDispatchers.IO) private val ioDispatcher: CoroutineDispatcher,
) : SignUpRepository {
override fun signUp(
signUp: SignUp,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.nativeapptemplate.nativeapptemplatefree.data.shop

import com.nativeapptemplate.nativeapptemplatefree.datastore.NatPreferencesDataSource
import com.nativeapptemplate.nativeapptemplatefree.datastore.NativeAppTemplatePreferencesDataSource
import com.nativeapptemplate.nativeapptemplatefree.model.*
import com.nativeapptemplate.nativeapptemplatefree.network.Dispatcher
import com.nativeapptemplate.nativeapptemplatefree.network.NatDispatchers
import com.nativeapptemplate.nativeapptemplatefree.network.NativeAppTemplateDispatchers
import com.nativeapptemplate.nativeapptemplatefree.network.emitApiResponse
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.first
Expand All @@ -12,9 +12,9 @@ import kotlinx.coroutines.flow.flowOn
import javax.inject.Inject

class ShopRepositoryImpl @Inject constructor(
private val natPreferencesDataSource: NatPreferencesDataSource,
private val natPreferencesDataSource: NativeAppTemplatePreferencesDataSource,
private val api: ShopApi,
@Dispatcher(NatDispatchers.IO) private val ioDispatcher: CoroutineDispatcher,
@Dispatcher(NativeAppTemplateDispatchers.IO) private val ioDispatcher: CoroutineDispatcher,
) : ShopRepository {

override fun getShops() = flow {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import kotlinx.coroutines.flow.map
import java.io.IOException
import javax.inject.Inject

class NatPreferencesDataSource @Inject constructor(
class NativeAppTemplatePreferencesDataSource @Inject constructor(
private val userPreferences: DataStore<UserPreferences>,
) {
val userData = userPreferences.data
Expand Down Expand Up @@ -77,7 +77,7 @@ class NatPreferencesDataSource @Inject constructor(
}
}
} catch (ioException: IOException) {
Log.e("NatPreferences", "Failed to update user preferences", ioException)
Log.e("NativeAppTemplatePreferences", "Failed to update user preferences", ioException)
throw ioException
}
}
Expand All @@ -93,7 +93,7 @@ class NatPreferencesDataSource @Inject constructor(
}
}
} catch (ioException: IOException) {
Log.e("NatPreferences", "Failed to update user preferences", ioException)
Log.e("NativeAppTemplatePreferences", "Failed to update user preferences", ioException)
throw ioException
}
}
Expand All @@ -112,7 +112,7 @@ class NatPreferencesDataSource @Inject constructor(
}
}
} catch (ioException: IOException) {
Log.e("NatPreferences", "Failed to update user preferences", ioException)
Log.e("NativeAppTemplatePreferences", "Failed to update user preferences", ioException)
throw ioException
}
}
Expand All @@ -125,7 +125,7 @@ class NatPreferencesDataSource @Inject constructor(
}
}
} catch (ioException: IOException) {
Log.e("NatPreferences", "Failed to update user preferences", ioException)
Log.e("NativeAppTemplatePreferences", "Failed to update user preferences", ioException)
throw ioException
}
}
Expand All @@ -143,7 +143,7 @@ class NatPreferencesDataSource @Inject constructor(
}
}
} catch (ioException: IOException) {
Log.e("NatPreferences", "Failed to update user preferences", ioException)
Log.e("NativeAppTemplatePreferences", "Failed to update user preferences", ioException)
throw ioException
}
}
Expand All @@ -154,7 +154,7 @@ class NatPreferencesDataSource @Inject constructor(
it.copy { this.didShowTapShopBelowTip = didShowTapShopBelowTip }
}
} catch (ioException: IOException) {
Log.e("NatPreferences", "Failed to update user preferences", ioException)
Log.e("NativeAppTemplatePreferences", "Failed to update user preferences", ioException)
throw ioException
}
}
Expand All @@ -165,7 +165,7 @@ class NatPreferencesDataSource @Inject constructor(
it.copy { this.isEmailUpdated = isEmailUpdated }
}
} catch (ioException: IOException) {
Log.e("NatPreferences", "Failed to update user preferences", ioException)
Log.e("NativeAppTemplatePreferences", "Failed to update user preferences", ioException)
throw ioException
}
}
Expand All @@ -176,7 +176,7 @@ class NatPreferencesDataSource @Inject constructor(
it.copy { this.isMyAccountDeleted = isMyAccountDeleted }
}
} catch (ioException: IOException) {
Log.e("NatPreferences", "Failed to update user preferences", ioException)
Log.e("NativeAppTemplatePreferences", "Failed to update user preferences", ioException)
throw ioException
}
}
Expand All @@ -187,7 +187,7 @@ class NatPreferencesDataSource @Inject constructor(
it.copy { this.isShopDeleted = isShopDeleted }
}
} catch (ioException: IOException) {
Log.e("NatPreferences", "Failed to update user preferences", ioException)
Log.e("NativeAppTemplatePreferences", "Failed to update user preferences", ioException)
throw ioException
}
}
Expand All @@ -198,7 +198,7 @@ class NatPreferencesDataSource @Inject constructor(
it.toBuilder().clear().build()
}
} catch (ioException: IOException) {
Log.e("NatPreferences", "Failed to clear user preferences", ioException)
Log.e("NativeAppTemplatePreferences", "Failed to clear user preferences", ioException)
throw ioException
}
}
Expand Down
Loading
Loading