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
1 change: 0 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ All errors should use the `CodedError` interface. Error codes use the `NATA-XXXX
|-------|------|-------------|
| NATA-1xxx | App/general errors | Unexpected errors, catch-all |
| NATA-2xxx | API/network errors | HTTP request failures, parsing errors |
| NATA-3xxx | NFC/scan errors | NFC tag read/write/scan failures |

- 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
Expand Down
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,10 @@ NativeAppTemplate-Free-Android uses modern Android development tools and practic
- Email Confirmation
- Forgot Password
- CRUD Operations for Shops (Create/Read/Update/Delete)
- CRUD Operations for Shops' Nested Resource, Number Tags (ItemTags) (Create/Read/Update/Delete)
- CRUD Operations for Shops' Nested Resource, Item Tags (Create/Read/Update/Delete)
- Force App Version Update
- Force Privacy Policy Version Update
- Force Terms of Use Version Update
- Generate QR Code Image for Number Tags (ItemTags) with a Centered Number
- NFC features for Number Tags (ItemTags): Write Application Info to a Tag, Read a Tag, Background Tag Reading
- And more!

## NFC Tag Operations
Expand Down
9 changes: 0 additions & 9 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,5 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import com.nativeapptemplate.nativeapptemplatefree.NatConstants
import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription
import com.nativeapptemplate.nativeapptemplatefree.data.login.SignUpRepository
import com.nativeapptemplate.nativeapptemplatefree.model.SendResetPassword
import com.nativeapptemplate.nativeapptemplatefree.utils.Utility.validateEmail
import com.nativeapptemplate.nativeapptemplatefree.utils.Utility.isValidEmail
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
Expand Down Expand Up @@ -75,7 +75,7 @@ class ForgotPasswordViewModel @Inject constructor(
fun hasInvalidDataEmail(): Boolean {
if (uiState.value.email.isBlank()) return true

return !uiState.value.email.validateEmail()
return !uiState.value.email.isValidEmail()
}

fun updateEmail(newEmail: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import com.nativeapptemplate.nativeapptemplatefree.NatConstants
import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription
import com.nativeapptemplate.nativeapptemplatefree.data.login.SignUpRepository
import com.nativeapptemplate.nativeapptemplatefree.model.SendConfirmation
import com.nativeapptemplate.nativeapptemplatefree.utils.Utility.validateEmail
import com.nativeapptemplate.nativeapptemplatefree.utils.Utility.isValidEmail
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
Expand Down Expand Up @@ -75,7 +75,7 @@ class ResendConfirmationInstructionsViewModel @Inject constructor(
fun hasInvalidDataEmail(): Boolean {
if (uiState.value.email.isBlank()) return true

return !uiState.value.email.validateEmail()
return !uiState.value.email.isValidEmail()
}

fun updateEmail(newEmail: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import com.nativeapptemplate.nativeapptemplatefree.data.login.LoginRepository
import com.nativeapptemplate.nativeapptemplatefree.model.LoggedInShopkeeper
import com.nativeapptemplate.nativeapptemplatefree.model.Login
import com.nativeapptemplate.nativeapptemplatefree.model.Permissions
import com.nativeapptemplate.nativeapptemplatefree.utils.Utility.validateEmail
import com.nativeapptemplate.nativeapptemplatefree.utils.Utility.isValidEmail
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
Expand Down Expand Up @@ -94,7 +94,7 @@ class SignInEmailAndPasswordViewModel @Inject constructor(
fun hasInvalidDataEmail(): Boolean {
if (uiState.value.email.isBlank()) return true

return !uiState.value.email.validateEmail()
return !uiState.value.email.isValidEmail()
}

fun hasInvalidDataPassword(): Boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescriptio
import com.nativeapptemplate.nativeapptemplatefree.data.login.SignUpRepository
import com.nativeapptemplate.nativeapptemplatefree.model.SignUp
import com.nativeapptemplate.nativeapptemplatefree.model.TimeZones
import com.nativeapptemplate.nativeapptemplatefree.utils.Utility.validateEmail
import com.nativeapptemplate.nativeapptemplatefree.utils.Utility.isValidEmail
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
Expand Down Expand Up @@ -88,7 +88,7 @@ class SignUpViewModel @Inject constructor(
fun hasInvalidDataEmail(): Boolean {
if (uiState.value.email.isBlank()) return true

return !uiState.value.email.validateEmail()
return !uiState.value.email.isValidEmail()
}

fun hasInvalidDataPassword(): Boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import com.nativeapptemplate.nativeapptemplatefree.data.login.SignUpRepository
import com.nativeapptemplate.nativeapptemplatefree.model.SignUpForUpdate
import com.nativeapptemplate.nativeapptemplatefree.model.TimeZones
import com.nativeapptemplate.nativeapptemplatefree.model.UserData
import com.nativeapptemplate.nativeapptemplatefree.utils.Utility.validateEmail
import com.nativeapptemplate.nativeapptemplatefree.utils.Utility.isValidEmail
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
Expand Down Expand Up @@ -198,7 +198,7 @@ class ShopkeeperEditViewModel @Inject constructor(
fun hasInvalidDataEmail(): Boolean {
if (uiState.value.email.isBlank()) return true

return !uiState.value.email.validateEmail()
return !uiState.value.email.isValidEmail()
}

fun updateName(newName: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,12 @@ import java.time.ZoneId
import java.time.ZonedDateTime

object DateUtility {
fun ZonedDateTime.cardDateString(): String {
val dateTimeFormatter = DateTimeFormatterUtility.cardDateFormatter()
return this.format(dateTimeFormatter)
}

fun String.cardDateString(zoneId: ZoneId = ZoneId.systemDefault()): String {
if (this.isBlank()) return ""

val date = ZonedDateTime.parse(this).withZoneSameInstant(zoneId)
return date.cardDateString()
}

fun ZonedDateTime.cardTimeString(): String {
val dateTimeFormatter = DateTimeFormatterUtility.cardTimeFormatter()
return this.format(dateTimeFormatter)
}

fun String.cardTimeString(zoneId: ZoneId = ZoneId.systemDefault()): String {
if (this.isBlank()) return ""

val date = ZonedDateTime.parse(this).withZoneSameInstant(zoneId)
return date.cardTimeString()
}

fun ZonedDateTime.cardDateTimeString(): String {
return "${this.cardDateString()} ${this.cardTimeString()}"
}

fun String.cardDateTimeString(zoneId: ZoneId = ZoneId.systemDefault()): String {
if (this.isBlank()) return ""

val date = ZonedDateTime.parse(this).withZoneSameInstant(zoneId)
return date.cardDateTimeString()
val dateString = date.format(DateTimeFormatterUtility.cardDateFormatter())
val timeString = date.format(DateTimeFormatterUtility.cardTimeFormatter())
return "$dateString $timeString"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import com.nativeapptemplate.nativeapptemplatefree.R
import java.util.Locale

object Utility {
fun String.validateEmail(): Boolean {
fun String.isValidEmail(): Boolean {
return this.isNotEmpty() && android.util.Patterns.EMAIL_ADDRESS.matcher(this).matches()
}

Expand Down
4 changes: 2 additions & 2 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@
<string name="item_tag_name_placeholder">Buy milk</string>
<string name="label_add_item_tag">Add Item Tag</string>
<string name="add_item_tag_description">Add a new item tag and start changing the item tag status.</string>
<string name="item_tag_name_is_invalid">Tag name is invalid.</string>
<string name="item_tag_description_is_invalid">Tag description is invalid.</string>
<string name="item_tag_name_is_invalid">Item tag name is invalid.</string>
<string name="item_tag_description_is_invalid">Item tag description is invalid.</string>
<string name="item_tag_name_help">Name must be 1-%1$d characters.</string>
<string name="item_tag_description_help">Description must be 0-%1$d characters.</string>
<string name="mark_as_completed">Mark as completed</string>
Expand Down
4 changes: 0 additions & 4 deletions app/src/main/res/xml/filepaths.xml

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import org.robolectric.RobolectricTestRunner

/**
* These tests use Robolectric because the subject under test (the ViewModel) uses
* `String.validateEmail` which has a dependency on `android.util.Patterns.EMAIL_ADDRESS`.
* `String.isValidEmail` which has a dependency on `android.util.Patterns.EMAIL_ADDRESS`.
*/
@RunWith(RobolectricTestRunner::class)
class ForgotPasswordViewModelTest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import org.robolectric.RobolectricTestRunner

/**
* These tests use Robolectric because the subject under test (the ViewModel) uses
* `String.validateEmail` which has a dependency on `android.util.Patterns.EMAIL_ADDRESS`.
* `String.isValidEmail` which has a dependency on `android.util.Patterns.EMAIL_ADDRESS`.
*/
@RunWith(RobolectricTestRunner::class)
class ResendConfirmationInstructionsViewModelTest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import org.robolectric.RobolectricTestRunner

/**
* These tests use Robolectric because the subject under test (the ViewModel) uses
* `String.validateEmail` which has a dependency on `android.util.Patterns.EMAIL_ADDRESS`.
* `String.isValidEmail` which has a dependency on `android.util.Patterns.EMAIL_ADDRESS`.
*/
@RunWith(RobolectricTestRunner::class)
class SignInEmailAndPasswordViewModelTest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import org.robolectric.RobolectricTestRunner

/**
* These tests use Robolectric because the subject under test (the ViewModel) uses
* `String.validateEmail` which has a dependency on `android.util.Patterns.EMAIL_ADDRESS`.
* `String.isValidEmail` which has a dependency on `android.util.Patterns.EMAIL_ADDRESS`.
*/
@RunWith(RobolectricTestRunner::class)
class SignUpViewModelTest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import org.robolectric.RobolectricTestRunner

/**
* These tests use Robolectric because the subject under test (the ViewModel) uses
* `String.validateEmail` which has a dependency on `android.util.Patterns.EMAIL_ADDRESS`.
* `String.isValidEmail` which has a dependency on `android.util.Patterns.EMAIL_ADDRESS`.
*/
@RunWith(RobolectricTestRunner::class)
class ShopkeeperEditViewModelTest {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,108 +1,27 @@
package com.nativeapptemplate.nativeapptemplatefree.utils

import com.nativeapptemplate.nativeapptemplatefree.utils.DateUtility.cardDateString
import com.nativeapptemplate.nativeapptemplatefree.utils.DateUtility.cardDateTimeString
import com.nativeapptemplate.nativeapptemplatefree.utils.DateUtility.cardTimeString
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import java.time.ZoneId
import java.time.ZonedDateTime

class DateUtilityTest {

private val testZonedDateTime = ZonedDateTime.of(
2025,
3,
15,
14,
30,
0,
0,
ZoneId.of("UTC"),
)

// ZonedDateTime extension tests

@Test
fun zonedDateTime_cardDateString_formatsCorrectly() {
val result = testZonedDateTime.cardDateString()
// Locale-safe: verify it contains day and year
assertTrue(result.contains("15"))
assertTrue(result.contains("2025"))
}

@Test
fun zonedDateTime_cardTimeString_formatsCorrectly() {
assertEquals("14:30", testZonedDateTime.cardTimeString())
}

@Test
fun zonedDateTime_cardDateTimeString_combinesDateAndTime() {
val result = testZonedDateTime.cardDateTimeString()
assertTrue(result.contains("15"))
assertTrue(result.contains("2025"))
assertTrue(result.contains("14:30"))
fun string_cardDateTimeString_withUtcZone_formatsCorrectly() {
val dateString = "2025-06-15T14:30:00Z"
assertEquals("2025/06/15 14:30", dateString.cardDateTimeString(ZoneId.of("UTC")))
}

@Test
fun string_cardDateTimeString_returnsEmptyForBlankString() {
fun string_cardDateTimeString_blankReturnsEmpty() {
assertEquals("", "".cardDateTimeString())
}

// String extension tests with UTC zone

@Test
fun string_cardDateString_formatsIsoStringWithUtcZone() {
val isoString = "2025-03-15T14:30:00Z"
val result = isoString.cardDateString(ZoneId.of("UTC"))
assertTrue(result.contains("15"))
assertTrue(result.contains("2025"))
}

@Test
fun string_cardTimeString_formatsIsoStringWithUtcZone() {
val isoString = "2025-03-15T14:30:00Z"
assertEquals("14:30", isoString.cardTimeString(ZoneId.of("UTC")))
}

// Blank string tests

@Test
fun string_cardDateString_returnsEmptyForBlankString() {
assertEquals("", "".cardDateString())
}

@Test
fun string_cardTimeString_returnsEmptyForBlankString() {
assertEquals("", "".cardTimeString())
}

@Test
fun string_cardDateString_returnsEmptyForWhitespaceString() {
assertEquals("", " ".cardDateString())
}

@Test
fun string_cardTimeString_returnsEmptyForWhitespaceString() {
assertEquals("", " ".cardTimeString())
}

// Timezone conversion tests

@Test
fun string_cardDateString_convertsTimezoneCorrectly() {
// 2025-03-15T23:30:00Z in UTC is 2025-03-16 08:30 in Asia/Tokyo (+9)
val isoString = "2025-03-15T23:30:00Z"
val result = isoString.cardDateString(ZoneId.of("Asia/Tokyo"))
assertTrue(result.contains("16"))
assertTrue(result.contains("2025"))
}

@Test
fun string_cardTimeString_convertsTimezoneCorrectly() {
// 2025-03-15T14:30:00Z in UTC is 23:30 in Asia/Tokyo (+9)
val isoString = "2025-03-15T14:30:00Z"
assertEquals("23:30", isoString.cardTimeString(ZoneId.of("Asia/Tokyo")))
fun string_cardDateTimeString_convertsTimezone() {
// UTC 14:30 -> Tokyo (UTC+9) is 23:30 same day
val dateString = "2025-06-15T14:30:00Z"
assertEquals("2025/06/15 23:30", dateString.cardDateTimeString(ZoneId.of("Asia/Tokyo")))
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.nativeapptemplate.nativeapptemplatefree.utils

import com.nativeapptemplate.nativeapptemplatefree.utils.Utility.validateEmail
import com.nativeapptemplate.nativeapptemplatefree.utils.Utility.isValidEmail
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
Expand All @@ -10,25 +10,25 @@ import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
class UtilityTest {

// validateEmail tests
// isValidEmail tests

@Test
fun validateEmail_validEmail_returnsTrue() {
assertTrue("test@example.com".validateEmail())
fun isValidEmail_validEmail_returnsTrue() {
assertTrue("test@example.com".isValidEmail())
}

@Test
fun validateEmail_emptyString_returnsFalse() {
assertFalse("".validateEmail())
fun isValidEmail_emptyString_returnsFalse() {
assertFalse("".isValidEmail())
}

@Test
fun validateEmail_noAtSign_returnsFalse() {
assertFalse("testexample.com".validateEmail())
fun isValidEmail_noAtSign_returnsFalse() {
assertFalse("testexample.com".isValidEmail())
}

@Test
fun validateEmail_noDomain_returnsFalse() {
assertFalse("test@".validateEmail())
fun isValidEmail_noDomain_returnsFalse() {
assertFalse("test@".isValidEmail())
}
}
Loading