Skip to content

Commit 45f2f12

Browse files
author
Ajit Kumar
committed
feat/ios
1 parent 12e7d05 commit 45f2f12

776 files changed

Lines changed: 201407 additions & 962 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.dockerignore‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ node_modules/
55
# Build outputs
66
platforms/
77
plugins/
8-
www/css/build/
9-
www/js/build/
108

119
# IDE
1210
.vscode/

‎.github/workflows/ci.yml‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,74 @@ jobs:
107107
npm ci --no-audit --no-fund
108108
npm run lang check
109109
110+
ios:
111+
name: Acode iOS (${{ matrix.edition }})
112+
runs-on: macos-26
113+
timeout-minutes: 30
114+
permissions:
115+
contents: read
116+
strategy:
117+
fail-fast: false
118+
matrix:
119+
include:
120+
- edition: paid
121+
package: com.foxdebug.acode
122+
- edition: free
123+
package: com.foxdebug.acodefree
124+
steps:
125+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
126+
with:
127+
submodules: recursive
128+
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
129+
with:
130+
node-version: '24'
131+
cache: npm
132+
- name: Install dependencies
133+
run: npm ci --no-audit --no-fund
134+
- name: Select package
135+
run: npm pkg set name=${{ matrix.package }}
136+
- name: Prepare local iOS configuration
137+
run: cp platforms/ios/Config.xcconfig.example platforms/ios/Config.xcconfig
138+
- name: Select iPhone and iPad simulators with working StoreKit testing
139+
id: simulator
140+
run: |
141+
node <<'JS'
142+
const { execFileSync } = require('node:child_process');
143+
const { appendFileSync } = require('node:fs');
144+
const { devices } = JSON.parse(execFileSync('xcrun', ['simctl', 'list', 'devices', 'available', '--json']));
145+
// iOS 26.3–26.5 fails SKTestSession configuration in command-line runs:
146+
// https://developer.apple.com/forums/thread/826971
147+
const supported = ['iOS-18-6', 'iOS-26-2'].flatMap(version =>
148+
devices[`com.apple.CoreSimulator.SimRuntime.${version}`] || []);
149+
for (const [family, output] of [['iPhone', 'id'], ['iPad', 'ipad']]) {
150+
const device = supported.find(device => device.isAvailable &&
151+
device.deviceTypeIdentifier?.startsWith(`com.apple.CoreSimulator.SimDeviceType.${family}-`));
152+
if (!device) throw new Error(`Install an iOS 18.6 or 26.2 ${family} simulator for integration tests`);
153+
appendFileSync(process.env.GITHUB_OUTPUT, `${output}=${device.udid}\n`);
154+
}
155+
JS
156+
- name: Build and run simulator tests with SSH and FTP fixtures
157+
run: |
158+
python3 -m venv .ios-build/ssh-fixture-venv
159+
.ios-build/ssh-fixture-venv/bin/pip install -r tests/fixtures/ssh/requirements.txt
160+
.ios-build/ssh-fixture-venv/bin/python tests/fixtures/ssh/server.py -- npm run test:ios -- --target=${{ steps.simulator.outputs.id }}
161+
- name: Run iPad startup and native UI checks
162+
run: |
163+
scheme=$(node -p 'require("./package.json").name === "com.foxdebug.acodefree" ? "runnerFree" : "runner"')
164+
checks=()
165+
for test in AppBridgeTests SystemUITests KeyboardLayoutTests PlatformUITests HapticControlsTests DocumentsPickerTests ShareTests PreviewBrowserTests FilesBrowserTests BrowserBridgeTests; do
166+
checks+=("-only-testing:${scheme}Tests/$test")
167+
done
168+
if [ "$scheme" = runner ]; then
169+
checks+=("-only-testing:runnerUITests/AppIconUITests")
170+
fi
171+
xcodebuild -project platforms/ios/runner.xcodeproj -scheme "$scheme" \
172+
-configuration Debug -sdk iphonesimulator \
173+
-destination 'platform=iOS Simulator,id=${{ steps.simulator.outputs.ipad }}' \
174+
-derivedDataPath .ios-build -clonedSourcePackagesDirPath .ios-build/SourcePackages \
175+
-parallel-testing-enabled NO -collect-test-diagnostics never \
176+
"${checks[@]}" CODE_SIGNING_ALLOWED=YES CODE_SIGN_IDENTITY=- test-without-building
177+
110178
android:
111179
name: Acode Android (${{ matrix.variant }}, ${{ matrix.distribution }})
112180
runs-on: ubuntu-latest

‎.gitignore‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
node_modules
22
/build.json
3-
/www/build
43
/keystore.jks
54
/platforms/android/debug-signing.properties
65
/platforms/android/release-signing.properties
@@ -22,4 +21,9 @@ tsconfig.tsbuildinfo
2221
.pnpm-store/
2322
platforms/android/**/build/
2423
platforms/android/.gradle/
25-
platforms/android/app/src/main/assets/www/
24+
platforms/**/bundle/
25+
26+
.ios-build/
27+
**/Config.xcconfig
28+
platforms/ios/**/*.xcuserstate
29+
platforms/ios/**/xcuserdata/

‎CONTRIBUTING.md‎

Lines changed: 208 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ Java 27 support currently requires [Gradle 9.8](https://docs.gradle.org/9.8.0-rc
108108

109109
### Build Steps
110110

111+
Web sources follow the Proteus layout: `src/index.html` is the entry HTML and
112+
`src/res/` contains static artwork. Rspack writes the complete web bundle to
113+
`platforms/android/app/src/main/assets/bundle/` or `platforms/ios/runner/bundle/`.
114+
These directories are generated and ignored by Git. The dev server serves the
115+
selected platform's bundle; edit source files in `src/`, not generated files.
116+
111117
```bash
112118
# Clone the repository
113119
git clone --recurse-submodules https://github.com/Acode-Foundation/Acode.git
@@ -125,16 +131,217 @@ The APK will be at: `platforms/android/app/build/outputs/apk/<edition>/debug/app
125131
> [!NOTE]
126132
> `@codemirror/lsp-client` comes from the `codemirror-lsp-client` git submodule and is installed as a local `file:` dependency, so initialize the submodule before running `npm ci` — see [Troubleshooting](#-troubleshooting).
127133
134+
## iOS development (port in progress)
135+
136+
Use macOS with Xcode 26 and an installed iOS simulator runtime, plus Node.js 24.
137+
Copy `platforms/ios/Config.xcconfig.example` to `platforms/ios/Config.xcconfig`
138+
once on a new checkout. This ignored file is only for local signing settings such
139+
as `DEVELOPMENT_TEAM`; leave the team empty for simulator builds. Keep version,
140+
icon and other public build settings in the Xcode project.
141+
Keep `runner/PrivacyInfo.xcprivacy` aligned with native API use: it declares file
142+
metadata, app-local preferences and elapsed-time measurements. Both targets
143+
also declare the local capacity checks used by filesystem requests (`E174.1`).
144+
They include this resource automatically through the template's synchronized folder.
145+
Check the built app's root manifest when changing target resource membership.
146+
Use iOS 18.6 or 26.2 for StoreKit integration tests. iOS 26.3–26.5 has a
147+
[StoreKitTest configuration regression](https://developer.apple.com/forums/thread/826971)
148+
in command-line runs; CI selects an unaffected installed runtime.
149+
Android SDK and Java are not needed for an iOS-only build. Install Java to run the
150+
complete shared test suite, which also checks Android build configuration.
151+
152+
```bash
153+
npm ci
154+
npm run dev:ios
155+
```
156+
157+
Like the Proteus template, this prepares the web bundle and opens Xcode. Select
158+
the `runner` scheme for paid or `runnerFree` for free, select your connected iPhone,
159+
and press **Cmd+R** to build, sign and install. Signing uses your local
160+
`Config.xcconfig`. `npm start -- ios` also opens Xcode after preparing the bundle.
161+
`--device` is accepted for this flow; `--target` is reserved for scripted simulator
162+
runs. Keep the Mac and iPhone on the same local network and allow Acode's local
163+
network access for live reload. Swift changes require another **Cmd+R** in Xcode.
164+
165+
For a scripted simulator build, install and launch, pass its UUID explicitly:
166+
167+
```bash
168+
xcrun simctl list devices available
169+
npm run build -- ios dev
170+
npm start -- ios --target=<simulator-UUID>
171+
npm run dev:ios -- --target=<simulator-UUID>
172+
npm run test:ios -- --target=<simulator-UUID>
173+
```
174+
175+
For the SSH/SFTP and FTP/FTPS integration tests, run the same simulator command inside the
176+
local fixture. It uses disposable keys, TLS certificates and files, binds only to loopback, and
177+
does not execute shell commands on your Mac. The wrapper stops it after testing.
178+
Explicit and implicit FTPS use allocated listener ports; native tests route the
179+
port-990 profile to its listener while retaining TLS hostname verification.
180+
These integration cases are skipped when the fixture is absent; CI includes it.
181+
182+
```bash
183+
python3 -m venv .ios-build/ssh-fixture-venv
184+
.ios-build/ssh-fixture-venv/bin/pip install -r tests/fixtures/ssh/requirements.txt
185+
.ios-build/ssh-fixture-venv/bin/python tests/fixtures/ssh/server.py -- npm run test:ios -- --target=<simulator-UUID>
186+
```
187+
188+
The iOS SSH transport uses pinned libssh2 and OpenSSL Swift packages. Xcode
189+
resolves them automatically; keep `Package.resolved` in version control. FTP/FTPS
190+
uses vendored curl source and shares that OpenSSL package. Source provenance,
191+
configuration and update instructions are in
192+
[`platforms/ios/Packages/CCurl/README.md`](platforms/ios/Packages/CCurl/README.md).
193+
FTPS validates the server certificate and hostname; port 990 uses implicit TLS
194+
and other ports use explicit TLS, with encrypted data connections.
195+
196+
`dev:ios` serves the web bundle over HTTP on the Mac's local-network address.
197+
With a simulator `--target`, it binds to loopback and automatically rebuilds
198+
changed Swift sources. Both modes reload JavaScript changes. The app retains its
199+
`acode://localhost` origin. Normal API connections continue to validate TLS.
200+
201+
The native iOS workspace index uses the system SQLite library and the existing
202+
`fileIndex` API. Simulator tests exercise persistent scans, incremental updates,
203+
search/replace events, cancellation and the Search in Files UI. The index lives
204+
in the app's `Library/NoCloud/workspace-index.sqlite`; it is regenerated from
205+
workspaces and is excluded from backups. Remote providers keep their JavaScript
206+
file discovery and search path.
207+
208+
Keep iOS keyboard-mode adaptation in `src/platforms/ios/input.ts`; it restores
209+
field defaults for prompts and preserves input/autofill semantics. Native menu
210+
suppression uses `AppWebView` and UIKit's menu builder. Do not remove or replace
211+
private WebKit input views. Fullscreen tests exercise orientation and restoring
212+
the editor/preview size after WebKit moves the WebView between containers.
213+
Filename prompts keep corrections and suggestions disabled on iOS even in normal
214+
keyboard mode; retain ordinary text defaults and explicit capitalization options.
215+
On iPad, multitasking can prevent programmatic rotation. A rejected orientation
216+
request must leave fullscreen usable and clear the temporary orientation policy.
217+
CI reuses each edition's built tests for focused iPad startup, file-picker, sharing,
218+
Safari, preview and native UI checks. An iPad UUID also works with `test:ios` above.
219+
220+
`PreviewTransferTests` verifies pending-download cancellation, restoring the editor
221+
after closing a preview with an alert, and cache invalidation. Keep asynchronous
222+
navigation and dialog presentation disabled after a preview is closed. For manual
223+
preview smoke tests, serve an attachment link and an HTML file input: cancel once,
224+
download twice, then select the first file from Acode/Downloads and compare its
225+
bytes in the page. `PreviewUploadTests` completes a multi-file multipart upload
226+
through the native picker's public delegate and compares the selected names, file
227+
contents and serialized request bytes. Keep touch selection, interrupted transfers
228+
and external Files providers in manual checks.
229+
`PreviewDownloadTests` verifies that accepted downloads finish after the preview
230+
is closed and released, while broken responses remove partial files. Keep accepted
231+
transfers owned by `PreviewDownloadManager`, separate from preview UI lifetime.
232+
`SSHTransferTests` cancels a throttled download through the public SFTP bridge and
233+
checks reconnection. It also drops server connections during 8 MiB uploads and
234+
downloads, verifies request rejection and disconnected state, and checks that
235+
retries replace partial content. These tests need the loopback fixture above.
236+
`FTPTransferTests` checks disconnecting a throttled download and recovering after
237+
interrupted FTP/FTPS uploads and downloads in active and passive modes. Its
238+
fixture closes disposable connections mid-transfer; keep these checks local.
239+
240+
The same `package.json.name` selects the edition: paid maps to `app.acode`, free to
241+
`app.acode.free`. Override the iOS identifier with `ACODE_IOS_BUNDLE_ID` when needed.
242+
Version and build number come from `package.json`. Simulator builds use ad-hoc
243+
signing so Keychain services work without a distribution certificate.
244+
245+
The free edition uses the `runnerFree` Xcode target; the paid edition uses `runner`.
246+
Only `runnerFree` includes `platforms/ios/ads`, the Google Mobile Ads/UMP packages
247+
and advertising metadata. Debug builds use Google's iOS test units. Free release
248+
builds require `ACODE_IOS_ADMOB_APP_ID`, `ACODE_IOS_ADMOB_BANNER_ID`,
249+
`ACODE_IOS_ADMOB_INTERSTITIAL_ID` and `ACODE_IOS_ADMOB_REWARDED_ID`.
250+
See [iOS advertising](docs/ios-advertising.md) for consent testing, source provenance
251+
and validation limits. Run the normal script before building `runnerFree` in Xcode;
252+
it prepares the free target's metadata and the matching web bundle.
253+
254+
The icon picker uses UIKit alternate icons and the existing reward/Pro gates.
255+
`runner.icon` and the fifteen alternate app-icon sets use Acode's existing
256+
`src/res/icons` artwork, rasterized at 1024 pixels. Keep these checked-in resources
257+
aligned when changing the artwork; no generation hook runs during builds.
258+
The paid scheme includes a Settings interaction test that changes and restores
259+
the icon, including Apple's confirmation and portrait/landscape rotation on
260+
iPhone and iPad. Free native tests cover packaged icons
261+
and the bridge; shared tests cover reward and purchase gates.
262+
263+
The `System` file utilities retain Android's result shapes, newline semantics and
264+
nonrecursive deletion while restricting paths to the sandbox or granted Files
265+
folders. Reward-pass state uses Keychain. Android file-edit intents and launcher
266+
shortcuts are hidden on iOS; sharing and opening exported copies remain available.
267+
`PluginInstallTests` installs the disposable ZIP in `runnerTests/Fixtures` through
268+
the Plugins source prompt, exercising extraction, script loading, legacy APIs,
269+
plugin context and cleanup on both editions. The fixture is test-bundle-only.
270+
`DocumentsPickerTests` checks picker return values, cancellation and reload
271+
cleanup; `ShareTests` checks exported copies and share-sheet cleanup. They drive
272+
the real UIKit controllers through public delegates/completions. Keep native
273+
Files-provider selection and destination sharing in the manual smoke checks.
274+
Include Save to Files, opening the exported copy in Acode, and saving an edit back
275+
to that copy. Existing-file writes must use the file's own grant without resolving
276+
its parent folder; `internalFsWrite.test.js` covers this alongside creation flags
277+
and write failures. Incoming share tabs intentionally do not persist in sessions.
278+
`FilesBrowserTests` opens Documents and reads a selected file through the shared
279+
file-browser UI and filesystem API, including iOS storage capability checks.
280+
Keep iOS root-history restoration before device readiness so saved sessions and
281+
folder-tree paths use the current container. Only recorded roots that remain
282+
authorized may remap an older URL; do not infer ownership from a container UUID.
283+
`FileURLTests` checks encoded native URLs, metadata and binary WebView fetches for
284+
reserved filenames. Use the native `resolveLocalFileSystemURI` alias for encoded
285+
URLs; the app's `resolveLocalFileSystemURL` wrapper encodes raw paths itself.
286+
`FileSymlinkTests` checks that entry paths, deletion and moves preserve symlink
287+
identity and leave targets intact. Keep target authorization on reads even when
288+
entry metadata retains the link's name.
289+
Reuse `FileTransfer` for coordinated copies and moves. Native FileEntry replacement
290+
semantics are covered by `FileTransferTests`; the shared filesystem wrapper keeps
291+
its existing conflict checks. Successful moves notify file presenters using
292+
[`item(at:didMoveTo:)`](https://developer.apple.com/documentation/foundation/nsfilecoordinator/item(at:didmoveto:)).
293+
`FileContentsTests` covers ranged reads, binary-reader chunks, write offsets and
294+
negative truncation. Preserve Android's EOF and error behavior; invalid truncate
295+
lengths must never be converted into a successful zero-length write.
296+
`FileEntryTests` checks filesystem-root boundaries, child paths, creation flags,
297+
invalid names and capacity requests without allocating the requested space.
298+
For upgrade smoke tests, save a file and add it to Recents, reinstall the app
299+
without uninstalling it, then verify editing and Recents after the container moves.
300+
301+
The native `Iap` service uses StoreKit 2 with the existing callback API.
302+
`IapBridgeTests` loads `runnerTests/Iap.storekit` into StoreKitTest; the fixture
303+
ships only in the test bundle and does not configure normal app launches.
304+
Tests make local simulated purchases without an App Store account or real charges.
305+
The service accepts existing SKU strings unchanged. Configure matching products
306+
for the chosen production bundle before App Store testing. Transactions expose
307+
`store: "appstore"` and a signed JWS in `purchaseToken`/`signedTransactionInfo`;
308+
the backend must verify Apple transactions instead of sending them to Google Play.
309+
The Settings page includes an iOS-only Restore purchases action. Neither billing
310+
restrictions nor a missing App Store product enable the Android external checkout.
311+
Until the backend supports Apple orders and refunds, iOS disables new paid-plugin
312+
and sponsorship purchases. Free and account-owned plugins can still be installed
313+
directly, as dependencies and from backups; unowned paid plugins are skipped during
314+
restore. Keep those gates in `src/lib/platform.js` until the corresponding backend
315+
flows are verified. Local Pro purchase and restoration remain enabled.
316+
The Google Play rating action is hidden on iOS until its App Store listing is
317+
configured. `PlatformUITests` covers Settings/About interaction and copied device
318+
information; keep store links and restoration instructions specific to the platform.
319+
320+
`npm run build -- ios prod --device` builds an unsigned device app. Use
321+
`platforms/ios/runner.xcodeproj` in Xcode to configure your team, signing and device
322+
installation. Direct Xcode builds use the version defaults in `project.pbxproj`;
323+
the npm scripts supply the version from `package.json`. Tests and build output are under
324+
`.ios-build/`. `--skip-web` reuses an already-built web bundle.
325+
326+
See [docs/ios-port.md](docs/ios-port.md) before testing feature parity. App Store
327+
products, iOS advertising identifiers, physical-device validation and several
328+
native services are still pending.
329+
128330
## Native Android development
129331

130332
`platforms/android` is checked-in source: edit it directly in Android Studio. There is no platform generation or native plugin installation step.
131333

334+
`FileResourceTest` checks the real WebView request interceptor against local files
335+
whose names contain URI delimiters and percent escapes. Its file IO runs through
336+
the native background pool. Keep decoded filesystem paths as paths when creating
337+
their file URIs; parsing them as URL text loses literal filename characters.
338+
132339
- `platforms/android/app/src/main/java`: Acode runtime and shared native services.
133340
- `platforms/android/app/src/free`: advertising implementation and metadata.
134341
- `platforms/android/app/src/store`: billing and proot assets, excluded by `fdroid`.
135342
- `src/native`: typed native APIs imported by `src/native/index.ts`; `bridge(service)` binds promise-based actions to the shared transport.
136343
- `src/platforms/android` and `src/platforms/ios`: platform transports using the shared callback and binary protocol.
137-
- `platforms/ios`: retained iOS template for the future Acode port; Android services are not yet ported to Swift.
344+
- `platforms/ios`: iOS app, with the template's runtime in `runner` and native services in `runner/lib`. Simulator tests and native dependencies remain alongside the app. See the [port checklist](docs/ios-port.md) for remaining work.
138345
- `platforms/android/app/src/main/assets/services.json`: native service registration.
139346
- `package.json`: app ID (`name`), version and Android version code.
140347

‎_typos.toml‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ check-filename = true
55
extend-exclude = [
66
"node_modules",
77
"codemirror-lsp-client",
8-
"www",
98
"*.yaml",
109
".vscode",
1110
"fastlane",

‎biome.json‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444
"src/lang/**/*.json",
4545
"src/native/**/*.ts",
4646
"src/platforms/**/*.ts",
47-
"!www/**/*",
4847
"!fastlane/**/*",
4948
"!platforms/**/*"
5049
]

0 commit comments

Comments
 (0)