Example

import {
  Button,
  List,
  Navigation,
  NavigationStack,
  Script,
  Section,
  Text,
  Toggle,
  useEffect,
  useState,
  VStack,
} from "scripting"

const accuracyCycle: LocationAccuracy[] = [
  "best",
  "tenMeters",
  "hundredMeters",
  "kilometer",
  "threeKilometers",
  "bestForNavigation",
  "reduced",
]

const activityCycle: Location.ActivityType[] = [
  "other",
  "automotiveNavigation",
  "fitness",
  "otherNavigation",
  "airborne",
]

function nextOf<T>(items: T[], current: T): T {
  const idx = items.indexOf(current)
  return items[(idx + 1) % items.length]
}

function fmt(n: number, digits = 4): string {
  return Number.isFinite(n) ? n.toFixed(digits) : String(n)
}

// Show a hint when "Always" was requested but iOS only granted "When In Use".
function maybeWarnAlwaysUpgrade(
  requested: boolean,
  result: { mode: "always" | "whenInUse" }
) {
  if (requested && result.mode !== "always") {
    Dialog.alert({
      title: "Always not granted",
      message:
        'iOS did not prompt for "Always" upgrade. Open Settings → Privacy & ' +
        'Security → Location Services → Scripting → Always to enable it manually.'
    })
  }
}

function Example() {
  const dismiss = Navigation.useDismiss()

  // Streaming readouts
  const [latestLocation, setLatestLocation] = useState<LocationInfo | null>(null)
  const [latestHeading, setLatestHeading] = useState<Location.Heading | null>(null)
  const [streamingLocation, setStreamingLocation] = useState(false)
  const [streamingHeading, setStreamingHeading] = useState(false)

  // Settings (mirrored to component state for UI re-render)
  const [accuracy, setAccuracyState] = useState<LocationAccuracy>(Location.accuracy)
  const [activityType, setActivityTypeState] = useState<Location.ActivityType>(Location.activityType)
  const [allowsBg, setAllowsBg] = useState(Location.allowsBackgroundLocationUpdates)
  const [pausesAuto, setPausesAuto] = useState(Location.pausesLocationUpdatesAutomatically)
  const [showsIndicator, setShowsIndicator] = useState(Location.showsBackgroundLocationIndicator)
  const [distanceFilter, setDistanceFilterState] = useState(Location.distanceFilter)
  const [headingFilter, setHeadingFilterState] = useState(Location.headingFilter)

  // Cleanup streams on unmount.
  useEffect(() => {
    return () => {
      Location.removeLocationListener()
      Location.removeHeadingListener()
      Location.stopUpdatingLocation()
      Location.stopUpdatingHeading()
    }
  }, [])

  return <NavigationStack>
    <List
      navigationTitle={"Location"}
      navigationBarTitleDisplayMode={"inline"}
      toolbar={{
        cancellationAction: <Button
          title={"Done"}
          action={dismiss}
        />
      }}
    >
      {/* Read-only state */}
      <Section
        header={<Text>Status</Text>}
      >
        <VStack alignment={"leading"}>
          <Text font={"headline"}>isAuthorizedForWidgetUpdates</Text>
          <Text font={"caption"}>{String(Location.isAuthorizedForWidgetUpdates)}</Text>
        </VStack>
        <VStack alignment={"leading"}>
          <Text font={"headline"}>accuracy</Text>
          <Text font={"caption"}>{accuracy}</Text>
        </VStack>
        <VStack alignment={"leading"}>
          <Text font={"headline"}>activityType</Text>
          <Text font={"caption"}>{activityType}</Text>
        </VStack>
      </Section>

      {/* One-shot APIs */}
      <Section header={<Text>One-shot</Text>}>
        <Button
          title={"requestCurrent (cached if available)"}
          action={async () => {
            const loc = await Location.requestCurrent()
            Dialog.alert({
              message: loc
                ? `lat=${fmt(loc.latitude)}\nlng=${fmt(loc.longitude)}`
                : "No location returned."
            })
          }}
        />
        <Button
          title={"requestCurrent ({ forceRequest: true })"}
          action={async () => {
            const loc = await Location.requestCurrent({ forceRequest: true })
            Dialog.alert({
              message: loc
                ? `lat=${fmt(loc.latitude)}\nlng=${fmt(loc.longitude)}`
                : "No location returned."
            })
          }}
        />
        <Button
          title={"requestHeading"}
          action={async () => {
            const h = await Location.requestHeading()
            Dialog.alert({
              message: h
                ? `trueHeading=${fmt(h.trueHeading, 1)}\nmagnetic=${fmt(h.magneticHeading, 1)}`
                : "No heading available."
            })
          }}
        />
        <Button
          title={"pickFromMap"}
          action={async () => {
            const picked = await Location.pickFromMap()
            Dialog.alert({
              message: picked
                ? `lat=${fmt(picked.latitude)}\nlng=${fmt(picked.longitude)}`
                : "Cancelled."
            })
          }}
        />
        <Button
          title={"reverseGeocode (Apple Park)"}
          action={async () => {
            try {
              const placemarks = await Location.reverseGeocode({
                latitude: 37.334900,
                longitude: -122.009020,
              })
              const first = placemarks?.[0]
              Dialog.alert({
                message: first
                  ? `${first.name ?? ""}\n${first.locality ?? ""}, ${first.country ?? ""}`
                  : "No placemark found."
              })
            } catch (e) {
              Dialog.alert({ message: String(e) })
            }
          }}
        />
        <Button
          title={"geocodeAddress (\"1 Infinite Loop\")"}
          action={async () => {
            try {
              const placemarks = await Location.geocodeAddress({
                address: "1 Infinite Loop, Cupertino, CA",
              })
              const first = placemarks?.[0]
              Dialog.alert({
                message: first?.location
                  ? `lat=${fmt(first.location.latitude)}\nlng=${fmt(first.location.longitude)}`
                  : "No result."
              })
            } catch (e) {
              Dialog.alert({ message: String(e) })
            }
          }}
        />
      </Section>

      {/* Heading stream */}
      <Section
        header={<Text>Heading stream</Text>}
        footer={<Text>Toggle on to subscribe via addHeadingListener.</Text>}
      >
        <Toggle
          title={"startUpdatingHeading"}
          value={streamingHeading}
          onChanged={async (on) => {
            if (on) {
              try {
                const result = await Location.startUpdatingHeading({ requestAlwaysAuthorization: false })
                maybeWarnAlwaysUpgrade(false, result)
                Location.addHeadingListener(setLatestHeading)
                setStreamingHeading(true)
              } catch (e) {
                Dialog.alert({ message: String(e) })
              }
            } else {
              Location.removeHeadingListener(setLatestHeading)
              Location.stopUpdatingHeading()
              setStreamingHeading(false)
              setLatestHeading(null)
            }
          }}
        />
        <VStack alignment={"leading"}>
          <Text font={"headline"}>latest heading</Text>
          <Text font={"caption"}>
            {latestHeading
              ? `true=${fmt(latestHeading.trueHeading, 1)}° / mag=${fmt(latestHeading.magneticHeading, 1)}°`
              : "—"}
          </Text>
        </VStack>
      </Section>

      {/* Location stream */}
      <Section
        header={<Text>Location stream</Text>}
        footer={<Text>Toggle on to subscribe via addLocationListener.</Text>}
      >
        <Toggle
          title={"startUpdatingLocation (whenInUse)"}
          value={streamingLocation}
          onChanged={async (on) => {
            if (on) {
              try {
                const result = await Location.startUpdatingLocation({ requestAlwaysAuthorization: false })
                maybeWarnAlwaysUpgrade(false, result)
                Location.addLocationListener(setLatestLocation)
                setStreamingLocation(true)
              } catch (e) {
                Dialog.alert({ message: String(e) })
              }
            } else {
              Location.removeLocationListener(setLatestLocation)
              Location.stopUpdatingLocation()
              setStreamingLocation(false)
              setLatestLocation(null)
            }
          }}
        />
        <Button
          title={"Restart with requestAlwaysAuthorization"}
          action={async () => {
            Location.removeLocationListener(setLatestLocation)
            Location.stopUpdatingLocation()
            try {
              const result = await Location.startUpdatingLocation({ requestAlwaysAuthorization: true })
              maybeWarnAlwaysUpgrade(true, result)
              Location.addLocationListener(setLatestLocation)
              setStreamingLocation(true)
            } catch (e) {
              Dialog.alert({ message: String(e) })
            }
          }}
        />
        <VStack alignment={"leading"}>
          <Text font={"headline"}>latest location</Text>
          <Text font={"caption"}>
            {latestLocation
              ? `lat=${fmt(latestLocation.latitude)} / lng=${fmt(latestLocation.longitude)}`
              : "—"}
          </Text>
        </VStack>
      </Section>

      {/* Settings */}
      <Section
        header={<Text>Settings</Text>}
        footer={<Text>Tap a value to cycle through valid options.</Text>}
      >
        <Button
          title={`setAccuracy → ${accuracy}`}
          action={async () => {
            const next = nextOf(accuracyCycle, accuracy)
            await Location.setAccuracy(next)
            setAccuracyState(next)
          }}
        />
        <Button
          title={`setActivityType → ${activityType}`}
          action={() => {
            const next = nextOf(activityCycle, activityType)
            Location.setActivityType(next)
            setActivityTypeState(next)
          }}
        />
        <Toggle
          title={"allowsBackgroundLocationUpdates"}
          value={allowsBg}
          onChanged={(on) => {
            Location.setAllowsBackgroundLocationUpdates(on)
            setAllowsBg(on)
          }}
        />
        <Toggle
          title={"pausesLocationUpdatesAutomatically"}
          value={pausesAuto}
          onChanged={(on) => {
            Location.setPausesLocationUpdatesAutomatically(on)
            setPausesAuto(on)
          }}
        />
        <Toggle
          title={"showsBackgroundLocationIndicator"}
          value={showsIndicator}
          onChanged={(on) => {
            Location.setShowsBackgroundLocationIndicator(on)
            setShowsIndicator(on)
          }}
        />
        <Button
          title={`setDistanceFilter → ${distanceFilter}m`}
          action={() => {
            // -1 (none) → 0 → 10 → 100 → 500 → -1
            const presets = [-1, 0, 10, 100, 500]
            const idx = presets.indexOf(distanceFilter)
            const next = presets[(idx + 1) % presets.length]
            Location.setDistanceFilter(next)
            setDistanceFilterState(next)
          }}
        />
        <Button
          title={`setHeadingFilter → ${headingFilter}°`}
          action={() => {
            const presets = [-1, 1, 5, 15, 45]
            const idx = presets.indexOf(headingFilter)
            const next = presets[(idx + 1) % presets.length]
            Location.setHeadingFilter(next)
            setHeadingFilterState(next)
          }}
        />
      </Section>
    </List>
  </NavigationStack>
}

async function run() {
  await Navigation.present({
    element: <Example />
  })

  Script.exit()
}

run()