Skip to content
Open
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 AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ WordPress-iOS uses a modular architecture with the main app and separate Swift p
- The WordPress scheme uses `WordPressUnitTests.xctestplan` for the full unit test suite, including tests in the `Modules` Swift package.
- Add every unit test target to `WordPressUnitTests.xctestplan`.
- Run the full suite with `xcodebuild -workspace WordPress.xcworkspace -scheme WordPress -testPlan WordPressUnitTests test`. Do not use `swift test`.
- To verify changes end-to-end on an iOS simulator, follow @docs/simulator-sign-in.md to sign in to the app.
- To sign a Simulator into WordPress.com, run `make sim-login` (it targets the running simulator; the WordPress.com token comes from `~/.wpcom-token` or `WPCOM_TOKEN`, and it prompts if none is set). See @docs/simulator-sign-in.md for options and self-hosted sign-in.

@crazytonyli crazytonyli Jul 22, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it targets the running simulator

There may be multiple running simulators. Or, maybe I want a new one.

the WordPress.com token comes from ~/.wpcom-token or WPCOM_TOKEN

I feel like this belongs to personal configuration. What do you think?


Overall, I feel like the new script adds too many restrictions to what the agent can do intuitively. Currently, I can say "Create a new simulator, sign in to the test account (WP.com or a self-hosted site), perform this end-to-end verification on the Jetpack app", and the agent has enough info (from the AGENTS.md and the sign in doc) to help with that. With this new change, the agent can't get how to do that from the prompts themselves. I have not tried the new script, but I suspect the agent would read the makefile and the new script to understand what it does and then perform the actions.

The new script wraps an oneline simctl command, which AI agents should be able to see in the docs/simulator-sign-in.md. Maybe it solves other problems that I don't see?


### Important Considerations
- **Multi-site Support**: Code must handle both WordPress.com and self-hosted sites
Expand Down
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
.PHONY: help dependencies
.PHONY: help dependencies sim-login

help: ## Show available targets
@grep -E '^[a-zA-Z_-]+:.*##' $(MAKEFILE_LIST) | awk -F ':.*## ' '{printf " %-20s %s\n", $$1, $$2}'

dependencies: ## Download and cache Gutenberg XCFrameworks
./Scripts/download-gutenberg-xcframeworks.sh

sim-login: ## Sign an iOS Simulator into WordPress.com (vars: DEVICE, APP, RESET=1; token from ~/.wpcom-token)
./Scripts/sim-signin.sh $(if $(APP),--app $(APP)) $(if $(DEVICE),--device $(DEVICE)) $(if $(RESET),--reset) $(ARGS)
191 changes: 191 additions & 0 deletions Scripts/sim-signin.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
#!/bin/bash
#
# Sign an iOS Simulator into WordPress.com with a bearer token, in one command.
#
# Wraps `xcrun simctl launch`, passing the `-wpcom-token` launch argument the app reads on
# the login screen to finish WordPress.com sign-in automatically — no taps required.
# Build and install the app on the simulator first (e.g. from Xcode).

set -euo pipefail

usage() {
cat <<'EOF'
Sign an iOS Simulator into WordPress.com with a bearer token, in one command.

Usage:
Scripts/sim-signin.sh [options]

Options:
-a, --app <jetpack|wordpress> App to sign in (default: jetpack)
-d, --device <udid|name> Target simulator (default: the running one; prompts if several)
-r, --reset Uninstall and reinstall the app first, for a clean slate
-h, --help Show this help

Examples:
Scripts/sim-signin.sh # Jetpack, booted simulator
Scripts/sim-signin.sh --app wordpress # WordPress
Scripts/sim-signin.sh --reset # reinstall for a clean slate first

The WordPress.com bearer token is read from (in order):
1. WPCOM_TOKEN environment variable
2. ~/.wpcom-token file
3. otherwise the script prompts you to paste one (and offers to save it to ~/.wpcom-token)

It is deliberately NOT accepted as a command-line flag: a token passed on the command line
would be saved in your shell history. Set WPCOM_TOKEN or write ~/.wpcom-token once.
EOF
}

app="jetpack"
device=""
reset=false
token=""

require_value() {
# $1 = option name, $2 = remaining argument count ($#)
if [[ "$2" -lt 2 ]]; then
echo "error: $1 requires a value" >&2
exit 1
fi
}

resolve_device() {
# Set `device` to a booted simulator: the only one if just one is booted, otherwise
# prompt to choose. Errors if none are booted. Called only when --device was omitted.
local udids=() names=() line udid name
while IFS= read -r line; do
# `|| true` so a UUID-less line (or a SIGPIPE from `head` under `pipefail`) doesn't
# trip `set -e` and abort the whole script before the `continue` below can skip it.
udid=$(printf '%s\n' "$line" | grep -oiE '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' | head -1 || true)
[[ -z "$udid" ]] && continue
name=$(printf '%s\n' "$line" | sed -E 's/^[[:space:]]*//; s/[[:space:]]*\([0-9A-Fa-f-]{36}\).*$//')
udids+=("$udid")
names+=("$name")
done < <(xcrun simctl list devices booted | grep -F "(Booted)")

local n=${#udids[@]}
if [[ "$n" -eq 0 ]]; then
echo "error: no booted simulator. Boot one (open Simulator, or 'xcrun simctl boot <udid>'), or pass --device <udid>." >&2
exit 1
fi
if [[ "$n" -eq 1 ]]; then
device="${udids[0]}"
echo "Using the only booted simulator: ${names[0]} (${device})"
return
fi

echo "Multiple simulators are booted — choose one:" >&2
local i
for (( i = 0; i < n; i++ )); do
printf " %2d) %s (%s)\n" "$(( i + 1 ))" "${names[i]}" "${udids[i]}" >&2
done
local sel
while true; do
printf "Select a simulator [1-%d]: " "$n" >&2
if ! read -r sel; then
echo >&2
echo "error: no selection made; re-run with --device <udid>." >&2
exit 1
fi
if [[ "$sel" =~ ^[0-9]+$ ]] && [[ "$sel" -ge 1 ]] && [[ "$sel" -le "$n" ]]; then
device="${udids[sel - 1]}"
echo "Using ${names[sel - 1]} (${device})"
return
fi
echo " not a valid choice: '$sel'" >&2
done
}

reset_app() {
# A clean slate: uninstall the app (which removes its entire data container — Core Data,
# UserDefaults, caches, cookies) and reinstall the same bundle. Unlike the in-app
# `-ui-test-reset-everything` wipe, `simctl install` is synchronous, so there's no window to
# race before the sign-in launch, and it clears more than just Core Data + UserDefaults.
local app_bundle bundle_name
app_bundle=$(xcrun simctl get_app_container "$device" "$bundle_id" app 2>/dev/null || true)
if [[ -z "$app_bundle" || ! -d "$app_bundle" ]]; then
echo "error: can't reset — $app ($bundle_id) isn't installed on '$device'. Build and install it first (e.g. from Xcode)." >&2
exit 1
fi
bundle_name=$(basename "$app_bundle")

# Uninstall deletes the installed bundle in place, so stage a copy to reinstall from.
# `reset_staging` is intentionally global so the EXIT trap can clean it up at script exit.
reset_staging=$(mktemp -d)
trap 'rm -rf "$reset_staging"' EXIT
cp -R "$app_bundle" "$reset_staging/$bundle_name"

echo "Resetting $app (uninstall + reinstall for a clean slate)…"
xcrun simctl uninstall "$device" "$bundle_id"
xcrun simctl install "$device" "$reset_staging/$bundle_name"
}

while [[ $# -gt 0 ]]; do
case "$1" in
-a|--app) require_value "$1" "$#"; app="$2"; shift 2 ;;
-d|--device) require_value "$1" "$#"; device="$2"; shift 2 ;;
-r|--reset) reset=true; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "error: unknown argument '$1'" >&2; usage >&2; exit 1 ;;
esac
done

# The token is read from the WPCOM_TOKEN environment variable, then a ~/.wpcom-token file.
# It is intentionally not a command-line argument, to keep it out of shell history.
if [[ -z "$token" ]]; then
token="${WPCOM_TOKEN:-}"
fi
if [[ -z "$token" && -f "$HOME/.wpcom-token" ]]; then
token="$(tr -d '[:space:]' < "$HOME/.wpcom-token")"
fi

# Nothing found anywhere — prompt for one. Input is hidden, since it's a secret.
if [[ -z "$token" ]]; then
printf "No WordPress.com token found (WPCOM_TOKEN / ~/.wpcom-token).\n" >&2
printf "Paste a bearer token (hidden), or press Return to cancel: " >&2
read -rs token || true
printf "\n" >&2
token="$(printf '%s' "$token" | tr -d '[:space:]')"
if [[ -n "$token" ]]; then
printf "Token received (%s chars).\n" "${#token}" >&2
printf "Save it to ~/.wpcom-token for next time? [y/N] " >&2
read -r save_reply || true
if [[ "$save_reply" =~ ^[Yy]$ ]]; then
token_file="$HOME/.wpcom-token"
# umask keeps the new file owner-only; chmod covers an existing, looser file.
if (umask 077; printf '%s\n' "$token" > "$token_file"); then
chmod 600 "$token_file" 2>/dev/null || true
printf "Saved to %s (mode 600).\n" "$token_file" >&2
else
printf "warning: could not write %s; continuing without saving.\n" "$token_file" >&2
fi
fi
fi
fi

if [[ -z "$token" ]]; then
echo "error: no token — set WPCOM_TOKEN, write ~/.wpcom-token, or paste one when prompted" >&2
usage >&2
exit 1
fi

case "$app" in
jetpack) bundle_id="com.automattic.jetpack" ;;
wordpress) bundle_id="org.wordpress" ;;
*) echo "error: unknown app '$app' (expected 'jetpack' or 'wordpress')" >&2; exit 1 ;;
esac

# When no --device was given, target the running simulator (prompting if several are booted).
if [[ -z "$device" ]]; then
resolve_device
fi

echo "Signing $app ($bundle_id) into WordPress.com on simulator '$device'…"

if [[ "$reset" == true ]]; then
reset_app
fi

xcrun simctl launch --terminate-running-process "$device" "$bundle_id" -wpcom-token "$token"

echo "Done. The app signs in automatically from the login screen."
11 changes: 10 additions & 1 deletion WordPress/Classes/Login/WordPressDotComAuthenticator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ struct WordPressDotComAuthenticator {
rawValue: "WordPressDotComAuthenticatorCallbackURL"
)

/// A WordPress.com bearer token supplied as a launch argument, used to sign a Simulator in
/// without the web flow. Reads `-wpcom-token`, falling back to the legacy
/// `-ui-test-wpcom-token` argument for backward compatibility.
static var launchArgumentToken: String? {
let defaults = UserDefaults.standard
return defaults.string(forKey: "wpcom-token")
?? defaults.string(forKey: "ui-test-wpcom-token")
}

static func redirectURI(for scheme: String) -> String {
"\(scheme)://oauth2-callback"
}
Expand Down Expand Up @@ -131,7 +140,7 @@ struct WordPressDotComAuthenticator {

let token: String
do {
if let tokenLaunchArgument = UserDefaults.standard.string(forKey: "ui-test-wpcom-token") {
if let tokenLaunchArgument = Self.launchArgumentToken {
token = tokenLaunchArgument
} else {
token = try await authenticate(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ class RootViewCoordinator {
self.rootViewPresenter = nil

WordPressAppDelegate.shared?.autoSignInUITestSite()
WordPressAppDelegate.shared?.autoSignInWPComAccountFromLaunchArgumentIfNeeded()
}

private func createPresenter(_ appType: AppUIType) -> RootViewPresenter {
Expand Down
36 changes: 36 additions & 0 deletions WordPress/Classes/System/WordPressAppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ public class WordPressAppDelegate: UIResponder, UIApplicationDelegate {
private let remoteFeatureFlagStore = RemoteFeatureFlagStore()
private let remoteConfigStore = RemoteConfigStore()

/// Limits the `-wpcom-token` launch-argument sign-in to a single attempt per process,
/// so signing out (which returns to the login screen) doesn't immediately re-sign in.
private var didAttemptLaunchArgumentSignIn = false

private var mainContext: NSManagedObjectContext {
ContextManager.shared.mainContext
}
Expand Down Expand Up @@ -901,4 +905,36 @@ extension WordPressAppDelegate {
}
)
}

/// Completes WordPress.com sign-in automatically when a bearer token is supplied via the
/// `-wpcom-token` launch argument, so a Simulator can be signed in with a single
/// `simctl launch` and no taps on the login screen.
///
/// No-op when the argument is absent, a WordPress.com account is already signed in, or a
/// launch-argument sign-in was already attempted this process. The last case matters because
/// signing out returns to the login screen — without it, the still-present token would
/// immediately sign the account back in.
func autoSignInWPComAccountFromLaunchArgumentIfNeeded() {
guard WordPressDotComAuthenticator.launchArgumentToken != nil else {
return
}
guard !didAttemptLaunchArgumentSignIn else {
return
}
guard (try? WPAccount.lookupDefaultWordPressComAccount(in: ContextManager.shared.mainContext)) == nil else {
return
}
guard let presenter = window?.topmostPresentedViewController else {
return
}
didAttemptLaunchArgumentSignIn = true
Task { @MainActor in
guard await WordPressDotComAuthenticator().signIn(from: presenter, context: .default) != nil else {
return
}
// Creating the account isn't enough on its own — without this the app stays on the
// login screen. Swap the window root to the signed-in app (no epilogue, so no taps).
windowManager.showUI()
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I intentionally kept the sign in flow manual, mainly because I want to avoid introducing another code path to the sign in flow, like this function here. The current auto-sign flow only changes where to get the token/credentials (web or env var).

If we want to fully automated sign in, do you think we can move the logic to the view controller that shows the buttons? Something like:

func viewDidAppear() {
  if autoSignWPCom {
    continueWithWPComButtonAction()
  }
}

For self-hosted site auto-sign in, we can do similar, something like

.onAppear {
  self.siteAddress = siteUrlEnvVar()
  continueButtonAction()
}

With this direction, the sign in code path stays the same, manually or fully automated.


BTW, the autoSignInUITestSite is a left-over dead code and should be deleted.

}
29 changes: 25 additions & 4 deletions docs/simulator-sign-in.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,38 @@
# Simulator Sign-In

Pass credentials as launch arguments, then tap through the sign-in screen to complete sign-in.
Pass credentials as launch arguments. WordPress.com sign-in then completes automatically; the self-hosted flow still needs a couple of taps.

## WordPress.com account

Launch with a bearer token:
The quickest path is **`make sim-login`** — it signs the running simulator into WordPress.com, prompting you to choose when more than one simulator is booted, and to paste a token if none is configured:

```bash
make sim-login # sign the running simulator into Jetpack
make sim-login APP=wordpress # WordPress instead of Jetpack
make sim-login RESET=1 # reinstall the app first (clean slate)
make sim-login DEVICE=<udid> # target a specific simulator
```

`make sim-login` forwards to `Scripts/sim-signin.sh`, which you can run directly with the same options as flags — `--app`, `--device`, `--reset`:

```bash
Scripts/sim-signin.sh --app wordpress --reset
```

`--reset` uninstalls and reinstalls the app for a clean slate — more thorough than the in-app data wipe (it also clears caches and cookies) and race-free (the reinstall is synchronous). The app must already be installed.

The token is resolved from `WPCOM_TOKEN`, then `~/.wpcom-token`; if none is set the script prompts you to paste one (hidden) and offers to save it to `~/.wpcom-token` for next time. There's deliberately no command-line token flag — a token passed as an argument would be saved in your shell history. Set it once (export `WPCOM_TOKEN` in `~/.zshrc`, or write `~/.wpcom-token`) and you never pass it again.

Or launch directly with the bearer token. The app finishes sign-in automatically while it sits on the login screen — no taps required:

```bash
xcrun simctl launch --terminate-running-process booted org.wordpress \
-ui-test-wpcom-token <bearer-token>
-wpcom-token <bearer-token>
```

On the sign-in screen, tap **"Continue with WordPress.com"**.
The legacy `-ui-test-wpcom-token` argument is still accepted for backward compatibility.

This raw form also saves the token in your shell history — `make sim-login` avoids that, since you never type the token. (Either way the token appears briefly in `ps` while `simctl launch` runs; that's inherent to passing it as a launch argument.)

## Self-hosted site

Expand Down