Clarify and verify live location consent
This commit is contained in:
parent
ee0650bf32
commit
cd154766a0
|
|
@ -315,6 +315,13 @@ public final class AndroidClientInstrumentedTest {
|
|||
cancel.click();
|
||||
|
||||
waitForServiceState(false);
|
||||
onWebView()
|
||||
.withElement(findElement(Locator.ID, "marker"))
|
||||
.check(webMatches(getText(), containsString("tracking-cancelled")));
|
||||
assertFalse(
|
||||
"Cancelling live-location disclosure was reported as a technical error",
|
||||
device.hasObject(By.text("tracking-error"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -537,12 +544,19 @@ public final class AndroidClientInstrumentedTest {
|
|||
}
|
||||
|
||||
private ActivityScenario<MainActivity> launch(String path) {
|
||||
Intent intent = new Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(BuildConfig.BASE_URL + path),
|
||||
context,
|
||||
MainActivity.class
|
||||
);
|
||||
String targetUrl = BuildConfig.BASE_URL + path;
|
||||
Intent intent = new Intent(context, MainActivity.class);
|
||||
|
||||
if (AppLinkRoutePolicy.allows(targetUrl)) {
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse(targetUrl));
|
||||
} else {
|
||||
// Instrumentation-only fixture routes are intentionally outside the
|
||||
// production App Link allowlist. Debug builds expose a same-origin
|
||||
// initial URL extra specifically for these isolated test pages.
|
||||
intent.putExtra("initial_url", targetUrl);
|
||||
}
|
||||
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
return ActivityScenario.launch(intent);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -259,6 +259,12 @@ final class FixtureHttpServer implements Closeable {
|
|||
+ "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">"
|
||||
+ "<title>Who Need Help Android fixture</title></head><body>"
|
||||
+ "<h1 id=\"marker\">loaded:" + escapedPath + "</h1>"
|
||||
+ "<script>"
|
||||
+ "window.addEventListener('wnh:native-tracking-cancelled',()=>{"
|
||||
+ "document.getElementById('marker').textContent='tracking-cancelled'});"
|
||||
+ "window.addEventListener('wnh:native-tracking-error',()=>{"
|
||||
+ "document.getElementById('marker').textContent='tracking-error'});"
|
||||
+ "</script>"
|
||||
+ "<button id=\"location\" onclick=\"navigator.geolocation.getCurrentPosition("
|
||||
+ "()=>{document.getElementById('marker').textContent='location-granted';"
|
||||
+ "fetch('/geolocation-granted')},"
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ public final class MainActivity extends ComponentActivity {
|
|||
startPendingNativeTracking();
|
||||
} else {
|
||||
pendingNativeTracking = null;
|
||||
dispatchNativeTrackingError();
|
||||
dispatchNativeTrackingCancelled();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
@ -533,7 +533,7 @@ public final class MainActivity extends ComponentActivity {
|
|||
|
||||
private void cancelPendingNativeTracking() {
|
||||
pendingNativeTracking = null;
|
||||
dispatchNativeTrackingError();
|
||||
dispatchNativeTrackingCancelled();
|
||||
}
|
||||
|
||||
private void dismissTrackingDisclosure() {
|
||||
|
|
@ -1044,6 +1044,17 @@ public final class MainActivity extends ComponentActivity {
|
|||
);
|
||||
}
|
||||
|
||||
private void dispatchNativeTrackingCancelled() {
|
||||
if (webView == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
webView.evaluateJavascript(
|
||||
"window.dispatchEvent(new CustomEvent('wnh:native-tracking-cancelled'))",
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
private static boolean validAssignmentId(String value) {
|
||||
try {
|
||||
UUID.fromString(value);
|
||||
|
|
|
|||
|
|
@ -2556,7 +2556,11 @@ export const Hooks = {
|
|||
this.nativeError = () => {
|
||||
this.pushEvent("location-error", {})
|
||||
}
|
||||
this.nativeCancelled = () => {
|
||||
this.pushEvent("location-cancelled", {})
|
||||
}
|
||||
window.addEventListener("wnh:native-tracking-error", this.nativeError)
|
||||
window.addEventListener("wnh:native-tracking-cancelled", this.nativeCancelled)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -2589,6 +2593,9 @@ export const Hooks = {
|
|||
if (this.nativeError) {
|
||||
window.removeEventListener("wnh:native-tracking-error", this.nativeError)
|
||||
}
|
||||
if (this.nativeCancelled) {
|
||||
window.removeEventListener("wnh:native-tracking-cancelled", this.nativeCancelled)
|
||||
}
|
||||
if (this.watchId !== undefined) navigator.geolocation.clearWatch(this.watchId)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
|||
|> assign(:subscribed_assignment_id, nil)
|
||||
|> assign(:tracking_presence_key, nil)
|
||||
|> assign(:tracking_session_id, nil)
|
||||
|> assign(:tracking_pending, false)
|
||||
|> assign(:accept_confirmation_open?, false)
|
||||
|> assign(:native_client, native_client)
|
||||
|> assign(
|
||||
|
|
@ -116,10 +117,19 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
|||
def handle_info({:position_updated, user_id, position, evidence}, socket) do
|
||||
positions = Map.put(socket.assigns.positions, user_id, position)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign_positions(positions)
|
||||
|> assign_assignment_evidence(evidence)}
|
||||
socket =
|
||||
socket
|
||||
|> assign_positions(positions)
|
||||
|> assign_assignment_evidence(evidence)
|
||||
|> then(fn socket ->
|
||||
if user_id == socket.assigns.current_scope.user.id do
|
||||
assign(socket, :tracking_pending, false)
|
||||
else
|
||||
socket
|
||||
end
|
||||
end)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_info({:tracking_stopped, user_id}, socket) do
|
||||
|
|
@ -132,6 +142,7 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
|||
if user_id == socket.assigns.current_scope.user.id do
|
||||
socket
|
||||
|> assign(:tracking_active, false)
|
||||
|> assign(:tracking_pending, false)
|
||||
|> assign(:tracking_session_id, nil)
|
||||
|> maybe_untrack_browser_presence()
|
||||
else
|
||||
|
|
@ -289,6 +300,7 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
|||
{:noreply,
|
||||
socket
|
||||
|> assign(:tracking_active, true)
|
||||
|> assign(:tracking_pending, true)
|
||||
|> assign(:tracking_session_id, tracking_session.id)
|
||||
|> maybe_track_browser_presence()
|
||||
|> maybe_start_native_tracking()}
|
||||
|
|
@ -300,11 +312,21 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
|||
|
||||
def handle_event("location-update", params, socket) do
|
||||
case Tracking.update_position(socket.assigns.current_scope, socket.assigns.assignment, params) do
|
||||
{:ok, _} -> {:noreply, socket}
|
||||
{:ok, _} -> {:noreply, assign(socket, :tracking_pending, false)}
|
||||
{:error, _} -> {:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("location-cancelled", _, socket) do
|
||||
case stop_tracking(socket) do
|
||||
{:ok, stopped_socket} ->
|
||||
{:noreply, stopped_socket}
|
||||
|
||||
{:error, reason} ->
|
||||
{:noreply, put_action_flash(socket, :error, message(reason))}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("location-error", _, socket) do
|
||||
socket =
|
||||
case stop_tracking(socket) do
|
||||
|
|
@ -410,6 +432,7 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
|||
|> assign(:messages_cursor, nil)
|
||||
|> assign(:positions, %{})
|
||||
|> assign(:tracking_active, false)
|
||||
|> assign(:tracking_pending, false)
|
||||
|> assign(:tracking_session_id, nil)
|
||||
|> refresh_request_coordinates()
|
||||
|> put_action_flash(
|
||||
|
|
@ -502,6 +525,7 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
|||
{:ok,
|
||||
socket
|
||||
|> assign(:tracking_active, false)
|
||||
|> assign(:tracking_pending, false)
|
||||
|> assign(:tracking_session_id, nil)
|
||||
|> maybe_untrack_browser_presence()
|
||||
|> maybe_stop_native_tracking()}
|
||||
|
|
@ -702,6 +726,8 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
|||
do: Tracking.list_current_positions(socket.assigns.current_scope, assignment),
|
||||
else: %{}
|
||||
|
||||
tracking_pending = tracking_active && not Map.has_key?(positions, current_user_id)
|
||||
|
||||
reputations =
|
||||
[request.requester_id, assignment && assignment.helper_id]
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|
|
@ -744,6 +770,7 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
|||
|> assign(:positions, positions)
|
||||
|> assign(:request_coordinates, request_coordinates)
|
||||
|> assign(:tracking_active, tracking_active)
|
||||
|> assign(:tracking_pending, tracking_pending)
|
||||
|> assign(:message_form, to_form(%{"body" => ""}, as: :message))
|
||||
|> assign(:handover_form, to_form(%{"code" => ""}, as: :handover))
|
||||
|> assign(
|
||||
|
|
@ -820,6 +847,7 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
|||
|
||||
if tracking_was_active and not tracking_active do
|
||||
socket
|
||||
|> assign(:tracking_pending, false)
|
||||
|> assign(:tracking_session_id, nil)
|
||||
|> maybe_untrack_browser_presence()
|
||||
|> maybe_stop_native_tracking()
|
||||
|
|
@ -1789,8 +1817,42 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
|||
>
|
||||
{gettext("Share location")}
|
||||
</button>
|
||||
<div
|
||||
:if={@tracking_pending}
|
||||
id="pending-location-status"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
class="alert alert-info mt-4 items-start text-sm text-base-content"
|
||||
>
|
||||
<span class="loading loading-spinner loading-sm mt-0.5 shrink-0" aria-hidden="true"></span>
|
||||
<div>
|
||||
<p class="font-semibold">{gettext("Waiting for location permission…")}</p>
|
||||
<p class="mt-1 text-xs opacity-75">
|
||||
{gettext(
|
||||
"Complete the location permission prompt. If you cancel, no location will be shared."
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
:if={@tracking_active && !@tracking_pending}
|
||||
id="active-location-status"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
class="alert alert-success mt-4 items-start text-sm text-base-content"
|
||||
>
|
||||
<.icon name="hero-map-pin" class="mt-0.5 size-5 shrink-0" />
|
||||
<div>
|
||||
<p class="font-semibold">{gettext("Live location is being shared")}</p>
|
||||
<p class="mt-1 text-xs opacity-75">
|
||||
{gettext(
|
||||
"Only your matched participant can see your current position. Stop sharing to remove the stored coordinates."
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
:if={@tracking_active}
|
||||
:if={@tracking_active && !@tracking_pending}
|
||||
id="stop-location-button"
|
||||
phx-click="stop-tracking"
|
||||
class="btn btn-error btn-sm mt-4 w-full"
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -4,7 +4,17 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
|
|||
import Phoenix.LiveViewTest
|
||||
import WhoNeedHelp.AccountsFixtures
|
||||
|
||||
alias WhoNeedHelp.{Accounts, Activities, Catalog, Help, Messaging, Notifications, Trust}
|
||||
alias WhoNeedHelp.{
|
||||
Accounts,
|
||||
Activities,
|
||||
Catalog,
|
||||
Help,
|
||||
Messaging,
|
||||
Notifications,
|
||||
Tracking,
|
||||
Trust
|
||||
}
|
||||
|
||||
alias WhoNeedHelp.Repo
|
||||
alias WhoNeedHelp.Trust.AbuseSignal
|
||||
|
||||
|
|
@ -1464,6 +1474,19 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
|
|||
|
||||
assert_push_event(helper_view, "native-tracking-start", %{assignment_id: assignment_id})
|
||||
assert assignment_id == assignment.id
|
||||
assert has_element?(helper_view, "#pending-location-status[role='status']")
|
||||
refute has_element?(helper_view, "#active-location-status")
|
||||
refute has_element?(helper_view, "#stop-location-button")
|
||||
|
||||
render_hook(helper_view, "location-update", %{
|
||||
"latitude" => 50.4501,
|
||||
"longitude" => 30.5234,
|
||||
"accuracy_meters" => 12
|
||||
})
|
||||
|
||||
refute has_element?(helper_view, "#pending-location-status")
|
||||
assert has_element?(helper_view, "#active-location-status[role='status']")
|
||||
assert has_element?(helper_view, "#stop-location-button")
|
||||
|
||||
assert {:ok, _cancelled} =
|
||||
Help.cancel_request(user_scope_fixture(requester), request.id)
|
||||
|
|
@ -1494,6 +1517,9 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
|
|||
|> element("button[phx-click='start-tracking']")
|
||||
|> render_click()
|
||||
|
||||
assert has_element?(helper_view, "#pending-location-status[role='status']")
|
||||
refute has_element?(helper_view, "#active-location-status")
|
||||
|
||||
assert Repo.get_by!(WhoNeedHelp.Tracking.TrackingSession,
|
||||
assignment_id: assignment.id,
|
||||
user_id: helper.id,
|
||||
|
|
@ -1528,6 +1554,85 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
|
|||
stop_live_view(helper_view)
|
||||
end
|
||||
|
||||
test "Android disclosure cancellation quietly stops the empty tracking session", _context do
|
||||
category = Catalog.seed_defaults()
|
||||
requester = user_fixture(display_name: "Location requester")
|
||||
helper = user_fixture(display_name: "Location helper")
|
||||
|
||||
{:ok, request} =
|
||||
Help.create_request(user_scope_fixture(requester), request_attrs(category))
|
||||
|
||||
{:ok, assignment} = Help.accept_request(user_scope_fixture(helper), request.id)
|
||||
|
||||
helper_conn =
|
||||
build_conn()
|
||||
|> log_in_user(helper)
|
||||
|> put_connect_params(%{"client_type" => "android"})
|
||||
|
||||
{:ok, helper_view, _html} = live(helper_conn, ~p"/requests/#{request.id}")
|
||||
|
||||
helper_view
|
||||
|> element("#share-location-button")
|
||||
|> render_click()
|
||||
|
||||
assert_push_event(helper_view, "native-tracking-start", %{assignment_id: assignment_id})
|
||||
assert assignment_id == assignment.id
|
||||
assert has_element?(helper_view, "#pending-location-status[role='status']")
|
||||
refute has_element?(helper_view, "#stop-location-button")
|
||||
|
||||
html = render_hook(helper_view, "location-cancelled", %{})
|
||||
|
||||
refute html =~ "Location sharing could not start."
|
||||
assert has_element?(helper_view, "#share-location-button")
|
||||
refute has_element?(helper_view, "#pending-location-status")
|
||||
refute has_element?(helper_view, "#active-location-status")
|
||||
refute has_element?(helper_view, "#stop-location-button")
|
||||
|
||||
refute Repo.get_by(WhoNeedHelp.Tracking.TrackingSession,
|
||||
assignment_id: assignment.id,
|
||||
user_id: helper.id,
|
||||
active: true
|
||||
)
|
||||
|
||||
stopped_session =
|
||||
Repo.get_by!(WhoNeedHelp.Tracking.TrackingSession,
|
||||
assignment_id: assignment.id,
|
||||
user_id: helper.id
|
||||
)
|
||||
|
||||
assert stopped_session.ended_at
|
||||
assert stopped_session.sample_count == 0
|
||||
|
||||
stop_live_view(helper_view)
|
||||
end
|
||||
|
||||
test "Android reconnect keeps an empty active session in the permission-pending state",
|
||||
_context do
|
||||
category = Catalog.seed_defaults()
|
||||
requester = user_fixture(display_name: "Location requester")
|
||||
helper = user_fixture(display_name: "Location helper")
|
||||
|
||||
{:ok, request} =
|
||||
Help.create_request(user_scope_fixture(requester), request_attrs(category))
|
||||
|
||||
{:ok, assignment} = Help.accept_request(user_scope_fixture(helper), request.id)
|
||||
{:ok, _session} = Tracking.start_session(user_scope_fixture(helper), assignment)
|
||||
|
||||
helper_conn =
|
||||
build_conn()
|
||||
|> log_in_user(helper)
|
||||
|> put_connect_params(%{"client_type" => "android"})
|
||||
|
||||
{:ok, helper_view, _html} = live(helper_conn, ~p"/requests/#{request.id}")
|
||||
|
||||
assert_push_event(helper_view, "native-tracking-start", %{assignment_id: assignment_id})
|
||||
assert assignment_id == assignment.id
|
||||
assert has_element?(helper_view, "#pending-location-status[role='status']")
|
||||
refute has_element?(helper_view, "#stop-location-button")
|
||||
|
||||
stop_live_view(helper_view)
|
||||
end
|
||||
|
||||
test "regular users cannot enter moderation", %{conn: conn} do
|
||||
assert {:error, {:redirect, %{to: "/requests"}}} = live(conn, ~p"/moderation")
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user