Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
30c38f1
feat: implement mobile onboarding tutorial
AlperKartkaya May 9, 2026
1db307c
feat: implement structured onboarding with step-by-step navigation
AlperKartkaya May 9, 2026
e92cdc3
feat: enhance onboarding with target hints, custom panel placement, a…
AlperKartkaya May 9, 2026
ebd4c1b
feat: implement interactive onboarding pulse animation and integrate …
AlperKartkaya May 9, 2026
492e93d
refactor: remove onboarding steps related to the back navigation acti…
AlperKartkaya May 9, 2026
3923614
feat: update E2E onboarding tests with refined navigation logic
AlperKartkaya May 10, 2026
7b58ccb
feat: integrate mobile onboarding flow into app navigation and drawer…
AlperKartkaya May 10, 2026
6e6673c
feat: add practice help request support to onboarding flow and displa…
AlperKartkaya May 10, 2026
d2d2fa4
feat: extend mobile onboarding tutorial to cover all app navigation s…
AlperKartkaya May 10, 2026
101395a
feat: update request help health sharing consent
May 10, 2026
aba1628
fix: streamline onboarding completion, improve progress calculation, …
AlperKartkaya May 10, 2026
3f6aa52
test: fix request help and gathering area e2e fixtures
May 10, 2026
32aa666
fix: recognize search rescue help requests
May 10, 2026
c8ae50f
fix: preserve legacy availability matching tokens
May 10, 2026
c99cbb8
fix: avoid duplicate gathering areas status text
May 10, 2026
7330360
fix: resolve merge conflicts
AlperKartkaya May 10, 2026
9768486
Merge branch 'development' into feat/mobile-contextual-self-onboarding
AlperKartkaya May 10, 2026
bdc7206
feat:Refactor privacy settings and remove health visibility control
May 10, 2026
ebc7f2e
fix: prevent duplicate navigation stack entries by checking for exist…
AlperKartkaya May 10, 2026
604730e
feat: add theme-aware logo support for web and update Android app ico…
AlperKartkaya May 10, 2026
330a1a7
feat: add theme toggle to navigation bar
AlperKartkaya May 10, 2026
ca80861
fix: replace Fire Brigade with Search & Rescue in onboarding
AlperKartkaya May 10, 2026
058c4e9
Merge pull request #573 from bounswe/feat/privacy-location-sharing-se…
kckagancan May 10, 2026
a30a046
fix: race condition resulting in an e2e test failure
AlperKartkaya May 10, 2026
fa7d8b7
chore: add tsbuildinfo files to .gitignore
AlperKartkaya May 10, 2026
9c4bcdd
Merge pull request #575 from bounswe/fix/button-send-help-request-mal…
kckagancan May 10, 2026
f2f2ad1
Merge pull request #576 from bounswe/fix/issue-539-icon-related-issues
kckagancan May 10, 2026
6554483
Merge pull request #577 from bounswe/fix/move-dark-theme-toggle-web
kckagancan May 10, 2026
7e622aa
fix: merge conflicts b/w this branch and dev
AlperKartkaya May 10, 2026
b549929
fix: more merge conflicts
AlperKartkaya May 10, 2026
cf6ab6c
Merge branch 'development' into feat/mobile-contextual-self-onboarding
AlperKartkaya May 10, 2026
cfe390f
Merge pull request #572 from bounswe/feat/mobile-contextual-self-onbo…
kckagancan May 10, 2026
29220f6
fix(android): clear safe status when requesting help
kckagancan May 10, 2026
369cc85
fix(android): avoid redirecting guests from mark-safe action
kckagancan May 10, 2026
5b49036
fix: fallback status bug
kckagancan May 10, 2026
dc3eaae
Merge pull request #579 from bounswe/fix/safe-state-bug
erinc00 May 11, 2026
961ca1a
feat(android): improve home safety status UX
kckagancan May 11, 2026
51912f0
fix: active help request mark safe flow
kckagancan May 11, 2026
33b8cd4
fix: align current location and viewport refresh behavior
kckagancan May 11, 2026
cad02d6
fix: align resource map controls and request actions
kckagancan May 11, 2026
f9973ea
fix: failed e2e test and web location picker
kckagancan May 11, 2026
8e4e49f
fix: normalize search rescue request types
May 11, 2026
f71c506
Replaced current-day timestamps with Today/Yesterday labels
berat-sayin May 11, 2026
86eda24
Merge remote-tracking branch 'origin/development' into feat/request-h…
May 11, 2026
2a79e91
Merge pull request #584 from bounswe/fix/map-component-bugs
gulcetahtasiz May 11, 2026
f64bf00
fix: normalize existing demo help type seeds
May 11, 2026
fc718c8
Merge pull request #585 from bounswe/feature/add-today-yesterday-time…
kckagancan May 11, 2026
bb41727
fix: review points
kckagancan May 11, 2026
98a31c0
Merge pull request #582 from bounswe/feat/cancel-hr-when-safe
gulcetahtasiz May 11, 2026
a8e6395
Merge pull request #570 from bounswe/feat/request-help-health-consent
kckagancan May 11, 2026
256c937
Fix notification page timezone handling
berat-sayin May 11, 2026
c37b772
Merge pull request #586 from bounswe/fix/timezone-on-notifications
kckagancan May 11, 2026
41124e2
fix(web): simplify profile location picker
kckagancan May 11, 2026
2e78a6a
Merge development into cleanup/web-location-search-bar
kckagancan May 11, 2026
2485d0d
Merge pull request #588 from bounswe/cleanup/web-location-search-bar
erinc00 May 11, 2026
86a36b6
Fix safety circle check-in timestamp formatting
May 11, 2026
f5718c3
Defer help request creation until submit
May 11, 2026
d9f5810
Merge pull request #592 from bounswe/fix/issue-591-safety-circle-time…
kckagancan May 11, 2026
bad0e86
Merge pull request #593 from bounswe/fix/issue-589-defer-help-request…
kckagancan May 11, 2026
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: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ out/
web/.next/
web/out/
web/next-env.d.ts
*.tsbuildinfo

# General build outputs
dist/
Expand Down
11 changes: 5 additions & 6 deletions android/app/src/androidTest/java/com/neph/e2e/AndroidE2ETest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -45,20 +45,19 @@ class AndroidE2ETest {
clickableNode("Continue as Guest").performClick()

waitForText("I need help now")
composeRule.onAllNodesWithContentDescription("Open menu")[0].performClick()
composeRule.onAllNodesWithContentDescription("Open menu", useUnmergedTree = true)[0].performClick()
waitForClickable("Help Request Map")
clickableNode("Help Request Map").performClick()

waitForText("Showing waiting help requests by type and priority.")
composeRule.onAllNodesWithText("Help Request Map")[0].assertIsDisplayed()

composeRule.waitUntil(5_000) {
composeRule.waitUntil(15_000) {
contentDescriptionNodeCount("Crisis marker") == 2 &&
textNodeCount("First Aid") > 0 &&
textNodeCount("Shelter") > 0 &&
textNodeCount("Priority: High") > 0 &&
textNodeCount("Waiting Requests") > 0 &&
textNodeCount("Search and Rescue") == 0 &&
textNodeCount("sariyer") == 0
}
}
Expand Down Expand Up @@ -106,7 +105,7 @@ class AndroidE2ETest {
fun systemBack_whenStackCanPop_navigatesBackWithoutExitDialog() {
waitForClickable("Log In")
clickableNode("Log In").performClick()
waitForTag("login_email")
openEmailFormIfNeeded("login_email")

pressSystemBack()

Expand Down Expand Up @@ -182,13 +181,13 @@ class AndroidE2ETest {

private fun textNodeCount(text: String): Int {
return runCatching {
composeRule.onAllNodesWithText(text).fetchSemanticsNodes().size
composeRule.onAllNodesWithText(text, substring = true, useUnmergedTree = true).fetchSemanticsNodes().size
}.getOrDefault(0)
}

private fun contentDescriptionNodeCount(text: String): Int {
return runCatching {
composeRule.onAllNodesWithContentDescription(text, substring = true).fetchSemanticsNodes().size
composeRule.onAllNodesWithContentDescription(text, substring = true, useUnmergedTree = true).fetchSemanticsNodes().size
}.getOrDefault(0)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import com.neph.MainActivity
import com.neph.features.auth.data.AuthSessionStore
import com.neph.features.profile.data.ProfileData
Expand Down Expand Up @@ -61,7 +62,7 @@ class AuthenticatedSessionAndroidE2ETest {
waitForText("I need help now")
composeRule.onNodeWithText("I need help now").assertIsDisplayed()

composeRule.onNodeWithContentDescription("Open menu").performClick()
composeRule.onNodeWithContentDescription("Open menu", useUnmergedTree = true).performClick()
waitForClickable("Profile")
clickableNode("Profile").performClick()

Expand All @@ -75,7 +76,7 @@ class AuthenticatedSessionAndroidE2ETest {
waitForText("I need help now")
composeRule.onNodeWithText("I need help now").assertIsDisplayed()

composeRule.onNodeWithContentDescription("Open menu").performClick()
composeRule.onNodeWithContentDescription("Open menu", useUnmergedTree = true).performClick()
waitForClickable("Settings")
clickableNode("Settings").performClick()

Expand All @@ -84,7 +85,7 @@ class AuthenticatedSessionAndroidE2ETest {

waitForText("Profile visibility")
composeRule.onNodeWithText("Profile visibility").assertIsDisplayed()
composeRule.onNodeWithText("Save Privacy Settings").assertIsDisplayed()
composeRule.onNodeWithText("Save Privacy Settings").performScrollTo().assertIsDisplayed()

composeRule.activity.runOnUiThread {
composeRule.activity.onBackPressedDispatcher.onBackPressed()
Expand All @@ -101,7 +102,7 @@ class AuthenticatedSessionAndroidE2ETest {
waitForText("I need help now")
composeRule.onNodeWithText("I need help now").assertIsDisplayed()

composeRule.onNodeWithContentDescription("Open menu").performClick()
composeRule.onNodeWithContentDescription("Open menu", useUnmergedTree = true).performClick()
waitForClickable("Settings")
clickableNode("Settings").performClick()

Expand Down
26 changes: 25 additions & 1 deletion android/app/src/androidTest/java/com/neph/e2e/FakeNephBackend.kt
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,8 @@ class FakeNephBackend {
route == "/availability/status" && method == "GET" -> handleAvailabilityStatus(token)
route == "/availability/my-assignment" && method == "GET" -> handleCurrentAssignment(token)
route == "/help-requests" && method == "GET" -> handleHelpRequestList(token)
route.startsWith("/help-requests/") && route.endsWith("/status") && method == "PATCH" ->
handlePatchHelpRequestStatus(token, route, body)
route == "/help-requests/active" && method == "GET" -> handleActiveHelpRequests(uri)
route == "/location/tree" && method == "GET" -> handleLocationTree(uri)
route == "/gathering-areas/nearby" && method == "GET" -> handleNearbyGatheringAreas(uri)
Expand Down Expand Up @@ -359,7 +361,7 @@ class FakeNephBackend {
put(
JSONObject()
.put("requestId", "fake-map-assigned")
.put("type", "search_and_rescue")
.put("type", "search_rescue")
.put("status", "PENDING")
.put("urgencyLevel", "HIGH")
.put("createdAt", "2026-05-01T09:55:00.000Z")
Expand Down Expand Up @@ -755,6 +757,28 @@ class FakeNephBackend {
return JSONObject().put("requests", JSONArray())
}

private fun handlePatchHelpRequestStatus(token: String?, route: String, body: JSONObject?): JSONObject {
requireAuthorizedUser(token)
val requestId = route
.removePrefix("/help-requests/")
.removeSuffix("/status")
.trim()
val status = body?.requiredString("status") ?: "SYNCED"
return JSONObject().put(
"request",
JSONObject()
.put("id", requestId)
.put("status", status)
.put("helpTypes", JSONArray())
.put("affectedPeopleCount", 1)
.put("riskFlags", JSONArray())
.put("vulnerableGroups", JSONArray())
.put("location", JSONObject())
.put("contact", JSONObject())
.put("cancelledAt", if (status == "CANCELLED") Instant.now().toString() else JSONObject.NULL)
)
}

private fun profileResponseJson(user: FakeUserState, profile: FakeProfileState): JSONObject {
val expertiseArray = JSONArray()
if (!profile.profession.isNullOrBlank() || profile.expertiseAreas.isNotEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
package com.neph.e2e

import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.assertIsOn
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import com.neph.MainActivity
import com.neph.features.auth.data.AuthSessionStore
import com.neph.features.onboarding.data.MobileOnboardingStore
import com.neph.features.profile.data.ProfileData
import com.neph.features.profile.data.ProfileRepository
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.rules.TestRule

class MobileOnboardingAndroidE2ETest {
private val fakeBackend = FakeNephBackend()
private val seededProfile = FakeProfileState(
firstName = "Mina",
lastName = "Onboard",
phoneNumber = "+905551112244",
age = 28,
gender = "female",
height = 168.0,
weight = 58.0,
country = "Turkey",
city = "Istanbul",
address = "Kadıköy, Test Street 8"
)
private val environmentRule = NephE2ETestEnvironmentRule(fakeBackend) { context, backend ->
backend.seedVerifiedUser(
email = "mina.onboarding@example.com",
password = "Passw0rd!",
profile = seededProfile
)

AuthSessionStore.initialize(context)
AuthSessionStore.saveAccessToken("access-token-1", rememberMe = true, userId = "user-1")
ProfileRepository.initialize(context)
ProfileRepository.saveProfile(
ProfileData(
firstName = "Mina",
lastName = "Onboard",
fullName = "Mina Onboard",
email = "mina.onboarding@example.com"
)
)
MobileOnboardingStore.initialize(context)
MobileOnboardingStore.markPendingForCurrentUser()
}
private val composeRule = createAndroidComposeRule<MainActivity>()

@get:Rule
val ruleChain: TestRule = RuleChain
.outerRule(environmentRule)
.around(composeRule)

@Test
fun pendingAuthenticatedUser_canFollowGuidedCoreConcepts_once() {
reachAssignedRequestsPage()

continueThroughDrawerPage(
pageTitle = "Emergency Numbers",
menuTargetTag = "mobile_onboarding_target_menu_emergency_info",
menuInstruction = "Tap Emergency Numbers",
pageText = "Emergency Contact List"
)
continueThroughDrawerPage(
pageTitle = "Help Request Map",
menuTargetTag = "mobile_onboarding_target_menu_help_request_map",
menuInstruction = "Tap Help Request Map",
pageText = "Showing waiting help requests"
)
continueThroughDrawerPage(
pageTitle = "Nearby Users",
menuTargetTag = "mobile_onboarding_target_menu_nearby_users",
menuInstruction = "Tap Nearby Users",
pageText = "Based on your residential"
)
continueThroughDrawerPage(
pageTitle = "Gathering Areas",
menuTargetTag = "mobile_onboarding_target_menu_gathering_areas",
menuInstruction = "Tap Gathering Areas",
pageText = "Location-based assembly"
)
continueThroughDrawerPage(
pageTitle = "Safety Circles",
menuTargetTag = "mobile_onboarding_target_menu_safety_circles",
menuInstruction = "Tap Safety Circles",
pageText = "Create Circle"
)
continueThroughDrawerPage(
pageTitle = "Notifications",
menuTargetTag = "mobile_onboarding_target_menu_notifications",
menuInstruction = "Tap Notifications",
pageText = "Notifications"
)
continueThroughDrawerPage(
pageTitle = "Settings",
menuTargetTag = "mobile_onboarding_target_menu_settings",
menuInstruction = "Tap Settings",
pageText = "Appearance"
)

composeRule.onNodeWithTag("mobile_onboarding_finish").performClick()
waitUntilTagGone("mobile_onboarding_dialog")
waitForText("I need help now")

composeRule.activityRule.scenario.recreate()
waitForText("I need help now")
waitUntilTagGone("mobile_onboarding_dialog")
}

@Test
fun skippingTutorialMidway_returnsUserHome() {
reachAssignedRequestsPage()
continueThroughDrawerPage(
pageTitle = "Emergency Numbers",
menuTargetTag = "mobile_onboarding_target_menu_emergency_info",
menuInstruction = "Tap Emergency Numbers",
pageText = "Emergency Contact List"
)

composeRule.onNodeWithTag("mobile_onboarding_skip").performClick()
waitUntilTagGone("mobile_onboarding_dialog")
waitForText("I need help now")
}

private fun reachAssignedRequestsPage() {
waitForText("I need help now")
waitForTag("mobile_onboarding_dialog")
waitForTag("mobile_onboarding_welcome_message")
composeRule.onNodeWithTag("mobile_onboarding_title").assertTextEquals("Home Dashboard")
composeRule.onNodeWithTag("mobile_onboarding_back").assertIsNotEnabled()

composeRule.onNodeWithTag("home_request_help_action").performClick()
waitForGuideTitle("Request Help")
waitForText("Create a help request")
waitForTag("mobile_onboarding_target_search_rescue")

composeRule.onNodeWithTag("mobile_onboarding_target_search_rescue").performClick()
waitForGuideTitle("Risk Flags")
waitForText("You selected Search & Rescue")
waitForTag("mobile_onboarding_target_fire_risk")

composeRule.onNodeWithTag("mobile_onboarding_target_fire_risk").performScrollTo().performClick()
waitForGuideTitle("Confirmation")
waitForText("You marked fire")
waitForTag("mobile_onboarding_confirmation_checkbox")

composeRule.onNodeWithTag("mobile_onboarding_confirmation_checkbox").performScrollTo().performClick()
waitForGuideTitle("Send Help Request")
composeRule.onNodeWithTag("mobile_onboarding_confirmation_checkbox").assertIsOn()
waitForTag("mobile_onboarding_target_send_help_request")

composeRule.onNodeWithTag("mobile_onboarding_target_send_help_request").performScrollTo().performClick()
waitForGuideTitle("My Help Requests")
waitForText("No real help request was saved")
waitForText("Guide preview only")
waitForText("Search & Rescue")

composeRule.onNodeWithTag("mobile_onboarding_continue").performClick()
waitForGuideTitle("Open the Menu")
waitForTag("mobile_onboarding_target_menu")

composeRule.onNodeWithTag("mobile_onboarding_target_menu").performClick()
waitForGuideTitle("Assigned Request")
waitForText("Tap Assigned Request")
waitForTag("mobile_onboarding_target_assigned_request_menu")

composeRule.onNodeWithTag("mobile_onboarding_target_assigned_request_menu").performClick()
waitForGuideTitle("Assigned Requests")
waitForText("There is no assigned request for you right now.")
waitForText("No assigned request right now.")

}


private fun continueThroughDrawerPage(
pageTitle: String,
menuTargetTag: String,
menuInstruction: String,
pageText: String
) {
composeRule.onNodeWithTag("mobile_onboarding_continue").performClick()
waitForGuideTitle("Open the Menu")
waitForTag("mobile_onboarding_target_menu")

composeRule.onNodeWithTag("mobile_onboarding_target_menu").performClick()
waitForText(menuInstruction)
waitForTag(menuTargetTag)

composeRule.onNodeWithTag(menuTargetTag).performScrollTo().performClick()
waitForGuideTitle(pageTitle)
waitForText(pageText)
}

private fun waitForGuideTitle(title: String, timeoutMillis: Long = 15_000) {
composeRule.waitUntil(timeoutMillis) {
runCatching {
composeRule.onNodeWithTag("mobile_onboarding_title").assertTextEquals(title)
true
}.getOrDefault(false)
}
}

private fun waitForText(text: String, timeoutMillis: Long = 15_000) {
composeRule.waitUntil(timeoutMillis) {
runCatching {
composeRule.onAllNodesWithText(text, substring = true).fetchSemanticsNodes().isNotEmpty()
}.getOrDefault(false)
}
}

private fun waitForTag(tag: String, timeoutMillis: Long = 15_000) {
composeRule.waitUntil(timeoutMillis) {
hasTag(tag)
}
}

private fun waitUntilTagGone(tag: String, timeoutMillis: Long = 15_000) {
composeRule.waitUntil(timeoutMillis) {
!hasTag(tag)
}
}

private fun hasTag(tag: String): Boolean {
return runCatching {
composeRule.onAllNodesWithTag(tag).fetchSemanticsNodes().isNotEmpty()
}.getOrDefault(false)
}
}
Loading
Loading