Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot] 6bcdf543d6 chore(deps-dev): bump jsdom from 29.1.1 to 30.1.3
Bumps [jsdom](https://github.com/jsdom/jsdom) from 29.1.1 to 30.1.3.
- [Release notes](https://github.com/jsdom/jsdom/releases)
- [Commits](https://github.com/jsdom/jsdom/compare/v29.1.1...v30.1.3)

---
updated-dependencies:
- dependency-name: jsdom
  dependency-version: 30.1.3
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-25 11:19:47 +00:00
326 changed files with 246 additions and 9223 deletions
-191
View File
@@ -1,191 +0,0 @@
name: Native apps release
# Builds the signed store builds of the iOS and Android apps and uploads them:
# the Android App Bundle to a Google Play testing track, the iOS archive to
# App Store Connect (TestFlight). Promotion to production stays a manual step
# in both consoles.
#
# Runs only in liketrek/TREK, only by hand or on a version tag, and only after a
# required reviewer approves the `store` environment, which holds every secret
# (see wiki/Native-Apps.md for the list). Pull requests never reach it.
#
# Both stores get the same numbers: the version from client/package.json
# without a prerelease suffix, and a build number that only grows. A prerelease
# reuses its X.Y.Z with a higher build number and belongs on the internal track
# and in TestFlight only.
on:
workflow_dispatch:
inputs:
track:
description: Google Play track
type: choice
options: [internal, alpha, beta]
default: internal
push:
tags: ['v*']
permissions:
contents: read
concurrency:
group: native-release
cancel-in-progress: false
jobs:
android:
if: github.repository == 'liketrek/TREK'
runs-on: ubuntu-24.04
timeout-minutes: 40
environment: store
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
cache: npm
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
cache: gradle
- name: Version and build number
id: version
run: |
echo "version=$(node -p "require('./client/package.json').version.split('-')[0]")" >> "$GITHUB_OUTPUT"
echo "build=$((GITHUB_RUN_NUMBER + 1000))" >> "$GITHUB_OUTPUT"
- name: Install
run: npm ci --workspace=shared --workspace=client
- name: Build shared
run: npm run build --workspace=shared
- name: Build the address screen and sync it into the shell
working-directory: client
run: npm run cap:sync -- android
- name: Upload key
env:
KEYSTORE_B64: ${{ secrets.ANDROID_UPLOAD_KEYSTORE_B64 }}
run: printf '%s' "$KEYSTORE_B64" | base64 -d > "$RUNNER_TEMP/upload.jks"
- name: Build the signed App Bundle
working-directory: client/android
env:
ORG_GRADLE_PROJECT_trekKeystorePassword: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ORG_GRADLE_PROJECT_trekKeyAlias: ${{ secrets.ANDROID_KEY_ALIAS }}
ORG_GRADLE_PROJECT_trekKeyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: >-
./gradlew bundleRelease --no-daemon
-PtrekVersionName=${{ steps.version.outputs.version }}
-PtrekVersionCode=${{ steps.version.outputs.build }}
-PtrekKeystore="$RUNNER_TEMP/upload.jks"
- uses: actions/upload-artifact@v6
with:
name: trek-android-${{ steps.version.outputs.version }}-${{ steps.version.outputs.build }}
path: client/android/app/build/outputs/bundle/release/app-release.aab
retention-days: 30
# Play only accepts API uploads once one bundle was uploaded by hand
# (wiki/Native-Apps.md). `draft` until the app has left draft in the console.
- name: Upload to Google Play
uses: r0adkll/upload-google-play@e738b9dd8f2476ea806d921b64aacd24f34515a5 # v1.1.5
with:
serviceAccountJsonPlainText: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}
packageName: com.liketrek.trek
releaseFiles: client/android/app/build/outputs/bundle/release/app-release.aab
track: ${{ inputs.track || 'internal' }}
status: draft
- name: Remove the upload key
if: always()
run: rm -f "$RUNNER_TEMP/upload.jks"
ios:
if: github.repository == 'liketrek/TREK'
runs-on: macos-15
timeout-minutes: 60
environment: store
steps:
- uses: actions/checkout@v6
# App Store uploads must be built with Xcode 26; the image's default is older.
- name: Select Xcode 26
run: |
XCODE=$(ls -d /Applications/Xcode_26*.app | sort -V | tail -1)
test -n "$XCODE"
sudo xcode-select -s "$XCODE"
xcodebuild -version
- uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
cache: npm
- name: Version and build number
id: version
run: |
echo "version=$(node -p "require('./client/package.json').version.split('-')[0]")" >> "$GITHUB_OUTPUT"
echo "build=$((GITHUB_RUN_NUMBER + 1000))" >> "$GITHUB_OUTPUT"
- name: Install
run: npm ci --workspace=shared --workspace=client
- name: Build shared
run: npm run build --workspace=shared
- name: Build the address screen and sync it into the shell
working-directory: client
run: npm run cap:sync -- ios
- name: App Store Connect API key
env:
ASC_KEY_P8_B64: ${{ secrets.ASC_KEY_P8_B64 }}
run: printf '%s' "$ASC_KEY_P8_B64" | base64 -d > "$RUNNER_TEMP/AuthKey.p8"
- name: Archive
working-directory: client/ios/App
env:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
run: >-
xcodebuild archive
-project App.xcodeproj
-scheme App
-configuration Release
-destination 'generic/platform=iOS'
-archivePath "$RUNNER_TEMP/TREK.xcarchive"
MARKETING_VERSION=${{ steps.version.outputs.version }}
CURRENT_PROJECT_VERSION=${{ steps.version.outputs.build }}
DEVELOPMENT_TEAM="$APPLE_TEAM_ID"
-allowProvisioningUpdates
-authenticationKeyPath "$RUNNER_TEMP/AuthKey.p8"
-authenticationKeyID "$ASC_KEY_ID"
-authenticationKeyIssuerID "$ASC_ISSUER_ID"
# ExportOptions.plist says destination=upload, so this step sends the build
# to App Store Connect, where it shows up in TestFlight after processing.
- name: Export and upload to App Store Connect
working-directory: client/ios/App
env:
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
run: >-
xcodebuild -exportArchive
-archivePath "$RUNNER_TEMP/TREK.xcarchive"
-exportOptionsPlist ../ExportOptions.plist
-exportPath "$RUNNER_TEMP/export"
-allowProvisioningUpdates
-authenticationKeyPath "$RUNNER_TEMP/AuthKey.p8"
-authenticationKeyID "$ASC_KEY_ID"
-authenticationKeyIssuerID "$ASC_ISSUER_ID"
- name: Remove the API key
if: always()
run: rm -f "$RUNNER_TEMP/AuthKey.p8"
-132
View File
@@ -1,132 +0,0 @@
name: Native apps
permissions:
contents: read
# Builds the iOS and Android shells whenever they, their address screen or the
# Capacitor plugins change. Nobody on the team has to own a Mac for the Swift
# side to be compiled on every change, and every run leaves a debug APK to
# install on a phone.
#
# Unsigned on purpose: signing and store uploads need secrets that fork PRs do
# not get, and belong in a release workflow of their own.
on:
pull_request:
paths:
- 'client/android/**'
- 'client/ios/**'
- 'client/native-shell/**'
- 'client/src/native/trekShell.ts'
- 'client/src/native/serverAddress.ts'
- 'client/capacitor.config.ts'
- 'client/vite.native.config.js'
- 'client/scripts/check-store-metadata.mjs'
- 'client/fastlane/**'
- 'client/package.json'
- 'package-lock.json'
- 'shared/src/i18n/*/native.ts'
- '.github/workflows/native.yml'
workflow_dispatch:
concurrency:
group: native-${{ github.ref }}
cancel-in-progress: true
jobs:
# The store texts fit their limits and every listing image has its size and
# no alpha channel (client/fastlane/README.md). Plain Node, no install.
metadata:
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
- name: Check the store listings
run: node client/scripts/check-store-metadata.mjs
android:
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
cache: npm
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
cache: gradle
- name: Install
run: npm ci --workspace=shared --workspace=client
- name: Build shared
run: npm run build --workspace=shared
- name: Build the address screen and sync it into the shells
working-directory: client
run: npm run cap:sync -- android
# The release build runs R8 with the keep rules in proguard-rules.pro, so a
# rule that goes missing fails here rather than in the store build. It is
# unsigned: the upload key only exists in native-release.yml.
- name: Assemble debug and release APKs
working-directory: client/android
run: ./gradlew assembleDebug assembleRelease lintDebug --no-daemon
- uses: actions/upload-artifact@v6
with:
name: trek-android-debug
path: client/android/app/build/outputs/apk/debug/app-debug.apk
retention-days: 14
ios:
runs-on: macos-15
timeout-minutes: 40
steps:
- uses: actions/checkout@v6
# App Store uploads must be built with Xcode 26; the image's default is older.
- name: Select Xcode 26
run: |
XCODE=$(ls -d /Applications/Xcode_26*.app | sort -V | tail -1)
test -n "$XCODE"
sudo xcode-select -s "$XCODE"
xcodebuild -version
- name: Check the property lists
run: plutil -lint client/ios/App/App/Info.plist client/ios/ExportOptions.plist
- uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
cache: npm
- name: Install
run: npm ci --workspace=shared --workspace=client
- name: Build shared
run: npm run build --workspace=shared
- name: Build the address screen and sync it into the shells
working-directory: client
run: npm run cap:sync -- ios
- name: Build for the simulator
working-directory: client/ios/App
run: |
xcodebuild build \
-project App.xcodeproj \
-scheme App \
-configuration Debug \
-sdk iphonesimulator \
-destination 'generic/platform=iOS Simulator' \
CODE_SIGNING_ALLOWED=NO
+4 -7
View File
@@ -182,7 +182,7 @@ Most of what follows is an addon an admin switches on or off. Lists, Costs, Docu
#### 📱 Mobile and offline
- **Apps**: TREK Travel Planner for iPhone, iPad and Android connects to any TREK server, and the browser installs TREK as a standalone app with a themed status bar too
- **Installable**: iOS and Android straight from the browser, no App Store, standalone with a themed status bar
- **Phone layouts**: their own screens for every part of the app, with a bottom bar and dashboard widgets the user orders
- **Offline reads**: the app shell is precached and trips, places, tours, files and nearby places live in a per-user IndexedDB store, so a trip opens with no network
- **Offline writes**: changes queue and replay with an `X-Idempotency-Key`, and a stale edit waits for a keep-mine or keep-theirs choice
@@ -234,15 +234,12 @@ the details.
## Get started in 30 seconds
```bash
ENCRYPTION_KEY=$(openssl rand -hex 32) docker run -d --name trek -p 3000:3000 \
ENCRYPTION_KEY=$(openssl rand -hex 32) docker run -d -p 3000:3000 \
-e ENCRYPTION_KEY=$ENCRYPTION_KEY \
-e COOKIE_SECURE=false \
-v ./data:/app/data -v ./uploads:/app/uploads mauriceboe/trek
```
Open `http://localhost:3000`, or `http://<server-ip>:3000` from another machine. On first boot TREK seeds an admin account — if you set `ADMIN_EMAIL`/`ADMIN_PASSWORD` those are used, otherwise the credentials are printed to the container log (`docker logs trek`).
`COOKIE_SECURE=false` is only for trying TREK over plain HTTP: without it the browser drops the session cookie and the login does not stick. Once TREK sits behind HTTPS (see [Reverse Proxy](#reverse-proxy)), remove it.
Open `http://localhost:3000`. On first boot TREK seeds an admin account — if you set `ADMIN_EMAIL`/`ADMIN_PASSWORD` those are used, otherwise the credentials are printed to the container log (`docker logs trek`).
<div align="center">
@@ -300,7 +297,7 @@ See [`charts/README.md`](https://github.com/liketrek/TREK/blob/main/charts/READM
<h2 id="install-as-app-pwa">Install as App (PWA)</h2>
TREK works as a Progressive Web App, and the TREK Travel Planner app for iPhone, iPad and Android connects to the same server ([Native Apps](https://github.com/liketrek/TREK/wiki/Native-Apps)). To install the web app instead:
TREK works as a Progressive Web App — no App Store needed.
1. Open TREK in the browser (HTTPS required)
2. **iOS**: Share ▸ *Add to Home Screen*
+2 -2
View File
@@ -1,5 +1,5 @@
apiVersion: v2
name: trek
version: 5.0.0
version: 4.3.4
description: Minimal Helm chart for TREK app
appVersion: "5.0.0"
appVersion: "4.3.4"
-4
View File
@@ -7,7 +7,3 @@ playwright/.cache/
# vite-plugin-pwa dev output (devOptions.enabled)
dev-dist/
e2e/help/media.env
# The native apps' address screen (npm run build:native), copied into
# android/ and ios/ by `cap sync`
native-dist/
-108
View File
@@ -1,108 +0,0 @@
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
# Built application files
*.apk
*.aar
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
# Keystore files and store credentials. The upload key and the Play service
# account live only in the release workflow's secrets and in the owner's
# offline backup, never in the repository.
*.jks
*.keystore
keystore.properties
*service-account*.json
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/
# Google Services (e.g. APIs or Firebase)
# Push is not wired up and the build does not apply the google-services plugin,
# so no CI secret is needed. Kept ignored so a stray Firebase config never lands
# in the repository.
google-services.json
app/google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Version control
vcs.xml
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
# Android Profiling
*.hprof
# Cordova plugins for Capacitor
capacitor-cordova-android-plugins
# Copied web assets
app/src/main/assets/public
# Generated Config files
app/src/main/assets/capacitor.config.json
app/src/main/assets/capacitor.plugins.json
app/src/main/res/xml/config.xml
-2
View File
@@ -1,2 +0,0 @@
/build/*
!/build/.npmkeep
-66
View File
@@ -1,66 +0,0 @@
apply plugin: 'com.android.application'
android {
namespace = "com.liketrek.trek"
compileSdk = rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "com.liketrek.trek"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
// The release workflow (.github/workflows/native-release.yml) passes both:
// the version from client/package.json and a build number that only grows.
// Local and PR builds fall back to these placeholders.
versionCode((project.findProperty('trekVersionCode') ?: '1') as Integer)
versionName(project.findProperty('trekVersionName') ?: '0.0.0-dev')
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
}
}
// The upload key only exists in the release workflow, which decodes it from a
// secret and passes its path and passwords as Gradle properties. Every other
// build (local, fork, PR) stays unsigned and needs no secret.
signingConfigs {
release {
if (project.hasProperty('trekKeystore')) {
storeFile file(project.property('trekKeystore'))
storePassword project.property('trekKeystorePassword')
keyAlias project.property('trekKeyAlias')
keyPassword project.property('trekKeyPassword')
}
}
}
buildTypes {
release {
if (project.hasProperty('trekKeystore')) {
signingConfig signingConfigs.release
}
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
repositories {
flatDir{
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
implementation "androidx.browser:browser:$androidxBrowserVersion"
implementation project(':capacitor-android')
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
implementation project(':capacitor-cordova-android-plugins')
}
apply from: 'capacitor.build.gradle'
-25
View File
@@ -1,25 +0,0 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
}
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
implementation project(':capacitor-app')
implementation project(':capacitor-filesystem')
implementation project(':capacitor-haptics')
implementation project(':capacitor-keyboard')
implementation project(':capacitor-share')
implementation project(':capacitor-splash-screen')
implementation project(':capacitor-status-bar')
}
if (hasProperty('postBuildExtras')) {
postBuildExtras()
}
-38
View File
@@ -1,38 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
# TREK's own classes: TrekShellPlugin is reached from JavaScript by name and its
# methods through reflection, so R8 must neither rename nor drop them.
-keep class com.liketrek.trek.** { *; }
# Every Capacitor plugin: the bridge finds plugins by their annotation and calls
# the annotated methods reflectively.
-keep @com.getcapacitor.annotation.CapacitorPlugin public class * {
@com.getcapacitor.PluginMethod public <methods>;
}
# The WebView bridge: Capacitor's message handler is called from JavaScript
# through @JavascriptInterface, and every plugin class is instantiated by name.
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}
-keep public class * extends com.getcapacitor.Plugin
@@ -1,80 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation|density"
android:name=".MainActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Deep links.
The scheme is @string/custom_url_scheme (com.liketrek.trek).
launchMode is singleTask above, so a link delivers into the
running task via onNewIntent rather than starting a second copy
of the app on top of the first.
This is the return path for SSO and passkey sign-in: TrekShell
opens the server's login page in a Custom Tab, and after the
hand-off it redirects to com.liketrek.trek://auth?code=…, which
lands here and in TrekShellPlugin.handleOnNewIntent.
Not an App Link (autoVerify + https). App Links are verified
against a specific domain via assetlinks.json, and TREK is
self-hosted — there is no single domain to verify against. A
custom scheme is the only form that works for every user's
server. -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="@string/custom_url_scheme" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data>
</provider>
</application>
<!-- Permissions. Only what the WebView itself needs: the file picker's
camera option and navigator.geolocation for "locate me" on the map,
both granted at runtime through Capacitor's WebChromeClient. -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- COARSE is listed first because it is the weaker grant and is enough
for "what city am I in". -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- required="false" on purpose: TREK is a planner, and everything except
"locate me" works without GPS hardware. Marking it required would have
Play hide the app from tablets and other GPS-less devices. -->
<uses-feature android:name="android.hardware.location.gps" android:required="false" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
</manifest>
@@ -1,50 +0,0 @@
package com.liketrek.trek;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import com.getcapacitor.BridgeActivity;
public class MainActivity extends BridgeActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
registerPlugin(TrekShellPlugin.class);
// With a server picked, the WebView loads that server instead of the
// bundled address screen, so the UI arrives in the version the server
// runs and on its own origin. BridgeActivity reads `config` while it
// builds the bridge inside super.onCreate().
boolean restarting = TrekServerStore.consumeRestart(this);
String server = TrekServerStore.getUrl(this);
boolean onServer = server != null && !TrekServerStore.consumeShellOnce(this);
config = TrekServerStore.configFor(this, onServer ? server : null, restarting);
super.onCreate(savedInstanceState);
if (bridge == null) return;
// The UI asks for no overscroll effect (overscroll-behavior: none on html)
// and Chrome honours that, but the WebView draws the stretch on its root
// scroller natively. It scales the whole view, top bar and bottom dock included.
bridge.getWebView().setOverScrollMode(View.OVER_SCROLL_NEVER);
// Set before any WebView callback can run: they are posted to this thread.
if (onServer) bridge.setWebViewClient(new TrekWebViewClient(bridge, this::showAddressScreen));
}
private boolean leavingServer;
/**
* The server did not load: back to the address screen, which offers retry
* and change. A fresh task rather than recreate(), because this usually
* happens during launch, while the system splash is still up, and a
* recreated activity leaves that splash lying over the new page.
*/
private void showAddressScreen() {
// A failed page can report more than one error; restart only once.
if (leavingServer) return;
leavingServer = true;
TrekServerStore.showShellOnce(this);
TrekServerStore.markRestart(this);
runOnUiThread(() -> {
startActivity(new Intent(this, MainActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK));
overridePendingTransition(0, 0);
});
}
}
@@ -1,123 +0,0 @@
package com.liketrek.trek;
import android.content.Context;
import android.content.SharedPreferences;
import com.getcapacitor.CapConfig;
import com.getcapacitor.Logger;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import org.json.JSONException;
import org.json.JSONObject;
/** The server the user picked, and the Capacitor configuration that loads it. */
final class TrekServerStore {
private static final String PREFS = "trek_shell";
private static final String KEY_URL = "server_url";
private static final String KEY_SHELL_ONCE = "shell_once";
private static final String KEY_RESTART = "restart";
private static final String CONFIG_DIR = "trek-server-config";
private TrekServerStore() {}
static String getUrl(Context context) {
return prefs(context).getString(KEY_URL, null);
}
static void setUrl(Context context, String url) {
SharedPreferences.Editor editor = prefs(context).edit();
if (url == null) {
editor.remove(KEY_URL);
} else {
editor.putString(KEY_URL, url);
}
editor.commit();
}
/** Start the next launch on the bundled address screen, even with a server remembered. */
static void showShellOnce(Context context) {
prefs(context).edit().putBoolean(KEY_SHELL_ONCE, true).commit();
}
static boolean consumeShellOnce(Context context) {
return consume(context, KEY_SHELL_ONCE);
}
/** The next onCreate is the app restarting itself, not a launch. */
static void markRestart(Context context) {
prefs(context).edit().putBoolean(KEY_RESTART, true).commit();
}
static boolean consumeRestart(Context context) {
return consume(context, KEY_RESTART);
}
private static boolean consume(Context context, String key) {
boolean set = prefs(context).getBoolean(key, false);
if (set) prefs(context).edit().remove(key).commit();
return set;
}
/**
* The bundled capacitor.config.json, adjusted for this start. Capacitor only
* takes these settings from a config file, so this writes a copy into the
* app's private storage and loads that; null means the bundled one as it is.
*
* With a server: server.url set, and server.errorPath dropped, because that
* page would have no bridge on the server's origin (TrekWebViewClient
* handles a failed load instead).
*
* On a restart: no launch splash. The splash plugin shows it again after
* recreate() but cannot take it down when the page hides it early, which
* left it covering a fully rendered page. A restart has no launch to cover.
*/
static CapConfig configFor(Context context, String url, boolean restarting) {
if (url == null && !restarting) return null;
try {
JSONObject json = new JSONObject(readAsset(context, "capacitor.config.json"));
if (url != null) {
JSONObject server = child(json, "server");
server.put("url", url);
server.remove("errorPath");
}
if (restarting) {
child(child(json, "plugins"), "SplashScreen").put("launchShowDuration", 0);
}
File dir = new File(context.getFilesDir(), CONFIG_DIR);
if (!dir.isDirectory() && !dir.mkdirs()) throw new IOException("Cannot create " + dir);
try (FileOutputStream out = new FileOutputStream(new File(dir, "capacitor.config.json"))) {
out.write(json.toString().getBytes(StandardCharsets.UTF_8));
}
return CapConfig.loadFromFile(context, dir.getAbsolutePath());
} catch (IOException | JSONException ex) {
Logger.error("TrekShell", "Could not adjust the app config for " + url, ex);
return null;
}
}
private static JSONObject child(JSONObject parent, String key) throws JSONException {
JSONObject existing = parent.optJSONObject(key);
if (existing != null) return existing;
JSONObject created = new JSONObject();
parent.put(key, created);
return created;
}
private static String readAsset(Context context, String name) throws IOException {
try (InputStream in = context.getAssets().open(name); ByteArrayOutputStream out = new ByteArrayOutputStream()) {
byte[] buffer = new byte[8192];
int read;
while ((read = in.read(buffer)) != -1) out.write(buffer, 0, read);
return out.toString("UTF-8");
}
}
private static SharedPreferences prefs(Context context) {
return context.getSharedPreferences(PREFS, Context.MODE_PRIVATE);
}
}
@@ -1,149 +0,0 @@
package com.liketrek.trek;
import android.content.Intent;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import androidx.browser.customtabs.CustomTabsIntent;
import com.getcapacitor.JSObject;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.CapacitorPlugin;
import org.json.JSONObject;
/** The native half of client/src/native/trekShell.ts. */
@CapacitorPlugin(name = "TrekShell")
public class TrekShellPlugin extends Plugin {
/** How long to wait after returning from the browser for the redirect to land. */
private static final long CANCEL_GRACE_MS = 600;
private String authCallId;
private String authScheme;
private boolean authBrowserOpen;
private final Handler handler = new Handler(Looper.getMainLooper());
@PluginMethod
public void getServer(PluginCall call) {
String url = TrekServerStore.getUrl(getContext());
JSObject result = new JSObject();
result.put("url", url == null ? JSONObject.NULL : url);
call.resolve(result);
}
@PluginMethod
public void setServer(PluginCall call) {
String url = call.getString("url");
Uri parsed = url == null ? null : Uri.parse(url);
if (parsed == null || parsed.getHost() == null || !("https".equals(parsed.getScheme()) || "http".equals(parsed.getScheme()))) {
call.reject("A http(s) URL is required");
return;
}
TrekServerStore.setUrl(getContext(), url);
call.resolve();
restart();
}
@PluginMethod
public void resetServer(PluginCall call) {
TrekServerStore.setUrl(getContext(), null);
call.resolve();
restart();
}
@PluginMethod
public void reload(PluginCall call) {
call.resolve();
restart();
}
/** The server URL is part of the bridge's configuration, so it takes a new activity. */
private void restart() {
TrekServerStore.markRestart(getContext());
getActivity().runOnUiThread(() -> getActivity().recreate());
}
@PluginMethod
public void authenticate(PluginCall call) {
String url = call.getString("url");
String scheme = call.getString("callbackScheme");
if (url == null || scheme == null) {
call.reject("url and callbackScheme are required");
return;
}
rejectPendingAuth("Superseded by a new sign-in");
call.setKeepAlive(true);
getBridge().saveCall(call);
authCallId = call.getCallbackId();
authScheme = scheme;
authBrowserOpen = true;
// A Custom Tab shares the browser's cookies, so a running SSO session at
// the identity provider carries over. The redirect back to the app's
// scheme arrives through the intent-filter in AndroidManifest.xml.
new CustomTabsIntent.Builder().build().launchUrl(getActivity(), Uri.parse(url));
}
@Override
protected void handleOnNewIntent(Intent intent) {
super.handleOnNewIntent(intent);
Uri data = intent.getData();
PluginCall call = pendingAuthCall();
if (call == null || data == null || !authScheme.equals(data.getScheme())) return;
JSObject result = new JSObject();
result.put("url", data.toString());
call.resolve(result);
finishAuth(call);
}
/**
* Back in the app without a redirect means the user closed the browser.
* onNewIntent runs before onResume when the redirect did arrive, so the
* grace period only covers devices that deliver the two out of order.
*/
@Override
protected void handleOnResume() {
super.handleOnResume();
if (!authBrowserOpen) return;
authBrowserOpen = false;
handler.postDelayed(() -> {
PluginCall call = pendingAuthCall();
if (call == null) return;
call.reject("Sign-in cancelled", "CANCELLED");
finishAuth(call);
}, CANCEL_GRACE_MS);
}
private PluginCall pendingAuthCall() {
return authCallId == null ? null : getBridge().getSavedCall(authCallId);
}
private void rejectPendingAuth(String message) {
PluginCall call = pendingAuthCall();
if (call == null) return;
call.reject(message, "CANCELLED");
finishAuth(call);
}
private void finishAuth(PluginCall call) {
getBridge().releaseCall(call);
authCallId = null;
authScheme = null;
authBrowserOpen = false;
}
/** iOS only: Android keeps the service worker, which already starts TREK offline. */
@PluginMethod
public void refreshOfflineCopy(PluginCall call) {
JSObject result = new JSObject();
result.put("saved", false);
call.resolve(result);
}
@PluginMethod
public void openOfflineCopy(PluginCall call) {
JSObject result = new JSObject();
result.put("opened", false);
call.resolve(result);
}
}
@@ -1,49 +0,0 @@
package com.liketrek.trek;
import android.webkit.WebResourceError;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
import com.getcapacitor.Bridge;
import com.getcapacitor.BridgeWebViewClient;
/**
* Used while the WebView shows the user's server. When that server cannot be
* loaded, Capacitor would show server.errorPath, but on Android it injects
* its bridge only into the server's origin, so that local page could neither
* retry nor change the server and stayed blank. Instead the app restarts on
* the bundled address screen, where the bridge is present and the screen
* offers exactly those two things.
*/
class TrekWebViewClient extends BridgeWebViewClient {
private final Runnable onUnreachable;
TrekWebViewClient(Bridge bridge, Runnable onUnreachable) {
super(bridge);
this.onUnreachable = onUnreachable;
}
@Override
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
if (request.isForMainFrame()) {
onUnreachable.run();
return;
}
super.onReceivedError(view, request, error);
}
/**
* Only a 5xx on the page itself means the server is down (a proxy's 502 or
* 503 while TREK restarts). A 4xx page is left alone: the server answered,
* and TREK serves its UI for every other path.
*/
@Override
public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {
if (!request.isForMainFrame()) {
super.onReceivedHttpError(view, request, errorResponse);
} else if (errorResponse.getStatusCode() >= 500) {
onUnreachable.run();
}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background>
<inset android:drawable="@mipmap/ic_launcher_background" android:inset="16.7%" />
</background>
<foreground>
<inset android:drawable="@mipmap/ic_launcher_foreground" android:inset="16.7%" />
</foreground>
<monochrome>
<inset android:drawable="@mipmap/ic_launcher_foreground" android:inset="16.7%" />
</monochrome>
</adaptive-icon>
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background>
<inset android:drawable="@mipmap/ic_launcher_background" android:inset="16.7%" />
</background>
<foreground>
<inset android:drawable="@mipmap/ic_launcher_foreground" android:inset="16.7%" />
</foreground>
<monochrome>
<inset android:drawable="@mipmap/ic_launcher_foreground" android:inset="16.7%" />
</monochrome>
</adaptive-icon>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#09090B</color>
<color name="launch_background">#09090B</color>
</resources>
@@ -1,7 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="app_name">TREK</string>
<string name="title_activity_main">TREK</string>
<string name="package_name">com.liketrek.trek</string>
<string name="custom_url_scheme">com.liketrek.trek</string>
</resources>
@@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:background">@null</item>
</style>
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
<item name="android:background">@drawable/splash</item>
<!-- Android 12+ draws its own launch screen: the app icon on this colour.
The same near-black as the icon tile and the first painted frame. -->
<item name="windowSplashScreenBackground">@color/launch_background</item>
</style>
</resources>
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path name="my_cache_images" path="." />
</paths>
@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
TREK is self-hosted: the user points the app at their own server, so we cannot
know its scheme or its certificate authority ahead of time. Two deliberate
relaxations, both required for that to work at all:
1. cleartextTrafficPermitted="true" — Android blocks http:// by default from
targetSdk 28 on. A bare LAN server (http://192.168.1.10:3001) and a
Tailscale host (100.64.0.0/10, which is NOT RFC1918, so a private-range
allowlist would not cover it) both need this. https:// is unaffected and
remains the recommended setup.
2. <certificates src="user" /> — since Android 7 apps ignore user-installed
CAs unless they opt in. Self-hosters running their own CA (step-ca, mkcert,
an internal PKI) install its root on the device; without this the app would
reject their perfectly valid https:// server. System CAs stay first so
public certs keep validating normally.
The tradeoff is that a user-installed CA can also be a MITM proxy's. That is
the same tradeoff the browser-based PWA already makes, and it is the price of
letting people run their own infrastructure.
-->
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</base-config>
</network-security-config>
-28
View File
@@ -1,28 +0,0 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.13.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
apply from: "variables.gradle"
allprojects {
repositories {
google()
mavenCentral()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
-24
View File
@@ -1,24 +0,0 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../../node_modules/@capacitor/android/capacitor')
include ':capacitor-app'
project(':capacitor-app').projectDir = new File('../../node_modules/@capacitor/app/android')
include ':capacitor-filesystem'
project(':capacitor-filesystem').projectDir = new File('../../node_modules/@capacitor/filesystem/android')
include ':capacitor-haptics'
project(':capacitor-haptics').projectDir = new File('../../node_modules/@capacitor/haptics/android')
include ':capacitor-keyboard'
project(':capacitor-keyboard').projectDir = new File('../../node_modules/@capacitor/keyboard/android')
include ':capacitor-share'
project(':capacitor-share').projectDir = new File('../../node_modules/@capacitor/share/android')
include ':capacitor-splash-screen'
project(':capacitor-splash-screen').projectDir = new File('../../node_modules/@capacitor/splash-screen/android')
include ':capacitor-status-bar'
project(':capacitor-status-bar').projectDir = new File('../../node_modules/@capacitor/status-bar/android')
-22
View File
@@ -1,22 +0,0 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
Binary file not shown.
@@ -1,7 +0,0 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-251
View File
@@ -1,251 +0,0 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
-94
View File
@@ -1,94 +0,0 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
-5
View File
@@ -1,5 +0,0 @@
include ':app'
include ':capacitor-cordova-android-plugins'
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
apply from: 'capacitor.settings.gradle'
-18
View File
@@ -1,18 +0,0 @@
ext {
minSdkVersion = 24
compileSdkVersion = 36
targetSdkVersion = 36
androidxActivityVersion = '1.11.0'
androidxAppCompatVersion = '1.7.1'
androidxCoordinatorLayoutVersion = '1.3.0'
androidxCoreVersion = '1.17.0'
androidxFragmentVersion = '1.8.9'
coreSplashScreenVersion = '1.2.0'
androidxWebkitVersion = '1.14.0'
junitVersion = '4.13.2'
androidxJunitVersion = '1.3.0'
androidxEspressoCoreVersion = '3.7.0'
cordovaAndroidVersion = '14.0.1'
// Custom Tabs for SSO and passkey sign-in (TrekShellPlugin.authenticate).
androidxBrowserVersion = '1.8.0'
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

-60
View File
@@ -1,60 +0,0 @@
/// <reference types="@capacitor/splash-screen" />
/// <reference types="@capacitor/keyboard" />
import type { CapacitorConfig } from '@capacitor/cli';
import { KeyboardResize } from '@capacitor/keyboard';
const config: CapacitorConfig = {
appId: 'com.liketrek.trek',
// What the home screen shows under the icon; the longer title belongs to
// the store listings.
appName: 'TREK',
// The app ships only the address screen. Once a server is picked, the native
// side points the WebView at it (TrekBridgeViewController.swift,
// MainActivity.java) and the UI comes from that server, always in the version
// the server runs. Session cookie, uploads, /ws and plugins work as they do in
// a browser, because the WebView is on the server's own origin.
webDir: 'native-dist',
server: {
// Android serves the address screen from https://localhost so it is a
// secure context. iOS keeps its default capacitor:// scheme: WKWebView
// handles https itself and Capacitor silently drops an iosScheme of https.
androidScheme: 'https',
// iOS shows this page from the app bundle whenever the server fails to
// load. It knows a server is remembered, first tries the stored offline
// copy of the server's start page, then offers to retry or pick another
// one. Android restarts on the same page instead (TrekWebViewClient.java),
// because it injects no bridge into a local page while a server is set.
errorPath: 'index.html',
},
ios: {
// Long-pressing a link shows a Safari preview otherwise, which no app does.
allowsLinkPreview: false,
},
plugins: {
SplashScreen: {
// Matches <meta name="theme-color"> in index.html, so the launch image,
// the splash and the first painted frame are the same colour and there is
// no white flash between them.
backgroundColor: '#09090b',
// The server's UI hides it once React has mounted (native/nativeApp.ts),
// the address screen once it has rendered. Auto-hide stays on as the
// ceiling, so a page that never gets there cannot leave it up for good.
launchAutoHide: true,
launchShowDuration: 4000,
launchFadeOutDuration: 200,
showSpinner: false,
androidScaleType: 'CENTER_CROP',
},
Keyboard: {
// The mobile shell is built from bottom sheets (mobile/screens/**/M*Sheet).
// `body` resizes the webview itself, so a sheet's own scroll container
// keeps working; `native` resizes the whole view and fights the sheet.
resize: KeyboardResize.Body,
// Android-only workaround: with an edge-to-edge status bar the webview
// otherwise does not resize at all when the keyboard opens.
resizeOnFullScreen: true,
},
},
};
export default config;
-72
View File
@@ -1,72 +0,0 @@
import { test, type Page } from '@playwright/test';
import { mkdirSync } from 'node:fs';
import path from 'node:path';
import { E2E_STATE_FILE } from '../../playwright.config';
import { clearNotices, seed } from '../screenshots/shot';
/**
* The store screenshots (`npm run store:shots`, then `npm run store:promote`).
*
* The same seeded demo trip the wiki pictures show, captured at the sizes App
* Store Connect and Google Play ask for: the iPhone 6.9" and iPad 13" displays,
* and a Play phone, 7" and 10" tablet. Below 768 px the phone shell answers, so
* the phone pictures show the phone UI; the tablets get the desktop layout they
* get on a real device.
*
* Raw captures go to e2e/.tmp/store/<device>/; scripts/store-screenshots.mjs
* flattens them (the stores refuse an alpha channel) and copies them into
* fastlane/. Nothing here asserts behaviour, and the run is not part of CI.
*/
const STORE_OUT_DIR = path.join(process.cwd(), 'e2e', '.tmp', 'store');
const DEVICES = [
{ name: 'iphone69', viewport: { width: 440, height: 956 }, deviceScaleFactor: 3, isMobile: true },
{ name: 'ipad13', viewport: { width: 1032, height: 1376 }, deviceScaleFactor: 2, isMobile: true },
{ name: 'play-phone', viewport: { width: 360, height: 640 }, deviceScaleFactor: 3, isMobile: true },
{ name: 'play-7in', viewport: { width: 810, height: 1440 }, deviceScaleFactor: 2, isMobile: true },
{ name: 'play-10in', viewport: { width: 1280, height: 720 }, deviceScaleFactor: 1.5, isMobile: true },
] as const;
/** The screens, in the order the listings show them. */
const SCREENS: { name: string; path: () => string }[] = [
{ name: 'dashboard', path: () => '/dashboard' },
{ name: 'plan', path: () => `/trips/${seed.tripId}?tab=plan` },
{ name: 'bookings', path: () => `/trips/${seed.tripId}?tab=buchungen` },
{ name: 'costs', path: () => `/trips/${seed.tripId}?tab=finanzplan` },
{ name: 'lists', path: () => `/trips/${seed.tripId}?tab=listen` },
{ name: 'journal', path: () => (seed.journeyId ? `/journey/${seed.journeyId}` : '/journey') },
];
/** Waits until the page has drawn what it is going to draw: fonts, images, no pending fade. */
async function settle(page: Page): Promise<void> {
await page.waitForLoadState('networkidle', { timeout: 5_000 }).catch(() => undefined);
await page.evaluate(async () => {
await document.fonts.ready;
});
await page.waitForTimeout(800);
}
for (const device of DEVICES) {
test.describe(device.name, () => {
test.use({
storageState: E2E_STATE_FILE,
viewport: device.viewport,
deviceScaleFactor: device.deviceScaleFactor,
isMobile: device.isMobile,
hasTouch: true,
});
SCREENS.forEach((screen, index) => {
test(`${index + 1} ${screen.name}`, async ({ page }) => {
const dir = path.join(STORE_OUT_DIR, device.name);
mkdirSync(dir, { recursive: true });
await page.goto(screen.path());
await clearNotices(page);
await settle(page);
await page.screenshot({ path: path.join(dir, `${index + 1}_${screen.name}.png`) });
});
});
});
}
-8
View File
@@ -31,14 +31,6 @@ export default tseslint.config(
'public',
'test-results',
'playwright-report',
// Capacitor native shells. `npx cap sync` copies the built address screen
// from native-dist/ into both platforms, so without these ESLint lints the
// minified output. gitignore() does not cover them: the iOS and Android
// .gitignore files live one level down, and this config's gitignore
// integration only reads client/.gitignore.
'native-dist',
'android/**',
'ios/**',
'e2e/**',
'scripts/**',
'**/*.config.js',
-55
View File
@@ -1,55 +0,0 @@
# Store listings
The texts, review notes, policy drafts and images for the App Store and Google
Play listings of **TREK Travel Planner** (`com.liketrek.trek`). The folder
layout is the one fastlane's `deliver` (App Store) and `supply` (Google Play)
read, so the listings can be uploaded from here; the builds themselves come from
`.github/workflows/native-release.yml`.
| Path | What |
| --- | --- |
| `metadata/en-US/` | App Store texts: name, subtitle, keywords, description, URLs, release notes |
| `metadata/review_information/` | App Review notes and contact. Placeholders only: the review server, accounts and passwords are entered in App Store Connect, never committed |
| `metadata/android/en-US/` | Google Play texts, the changelog, and `images/` (feature graphic, icon, screenshots) |
| `metadata/privacy-policy.md`, `metadata/delete-account.md` | Drafts of the pages published at https://liketrek.com/privacy and https://liketrek.com/delete-account |
| `screenshots/en-US/` | App Store screenshots, iPhone 6.9" and iPad 13" |
`npm run store:check` (in `client/`) checks every text against its store limit
and every image for its size and for an alpha channel, which both stores refuse.
CI runs it in the `metadata` job of `native.yml`.
## Screenshots
The screenshots are not committed yet: they are captured from a running TREK
with the seeded demo trip, which is a Playwright run and does not run in CI.
To make them, from `client/`:
1. `npx playwright install chromium` (once).
2. `npm run store:shots` starts its own backend on a throwaway database and the
Vite dev server, seeds the demo trip (the same one the wiki pictures show)
and captures six screens at five device sizes into `e2e/.tmp/store/`.
3. `npm run store:promote` checks each capture's size, flattens it onto the app
background (no alpha) and copies it into `screenshots/en-US/` (iPhone 6.9"
1320x2868, iPad 13" 2064x2752) and
`metadata/android/en-US/images/{phone,sevenInch,tenInch}Screenshots/`
(1080x1920, 1620x2880, 1920x1080).
4. `npm run store:graphic` rebuilds the feature graphic, now with a crop of the
first phone screenshot, and the 512 px icon.
5. `npm run store:check`, look through the pictures, commit.
The capture lives in `e2e/store/capture.store.ts`; the device sizes and screens
are listed at its top.
## Uploading the listing
With fastlane installed and the App Store Connect API key and Play service
account at hand (the same ones the release workflow uses, see
`wiki/Native-Apps.md`), from `client/`:
```bash
fastlane deliver --skip_binary_upload --metadata_path fastlane/metadata --screenshots_path fastlane/screenshots
fastlane supply --skip_upload_apk --skip_upload_aab --metadata_path fastlane/metadata/android --package_name com.liketrek.trek
```
Both also work from the consoles by hand; the files here are the source of
truth for what the listings say.
@@ -1 +0,0 @@
The first release of TREK for Android: connect to your TREK server, plan together, and take your trips offline.
@@ -1,24 +0,0 @@
TREK is a travel planner for the trips you take with other people. Plan the days together, see every place on the map, keep bookings, costs and packing lists in one place, and still have all of it with you when the connection drops.
TREK is open source and works with a TREK server: one you run yourself, at home or on a rented server, one your organisation provides, or the public demo to try it out. Your trips stay on that server. The makers of TREK receive none of your data.
PLAN TOGETHER
• Days with places, notes, transports and stays, in order and on the map
• Changes from everyone on the trip appear live
• Roles that decide who may change what
BOOKINGS AND COSTS
• Flights, trains, hotels, rentals and tickets, with their documents
• Shared expenses with fair splits and who owes whom, in any currency
ON THE ROAD
• Trips you choose are kept on the device, maps included, for when you are offline
• Packing lists and to-dos for the whole group
• A travel journal with photos, and an atlas of the countries you have visited
YOUR DATA
• No account with us, no tracking, no ads
• Sign in with a password, a passkey or your organisation's single sign-on
• Available in 27 languages
To use the app you need the address of a TREK server. Without one yet, tap "Try the demo" on the first screen, or read how to run your own at github.com/liketrek/TREK.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

@@ -1 +0,0 @@
Plan trips together on your own TREK server, with maps, bookings and costs.
@@ -1 +0,0 @@
TREK Travel Planner
-1
View File
@@ -1 +0,0 @@
2026 [PUBLISHER LEGAL NAME]
@@ -1,29 +0,0 @@
# TREK Travel Planner: Delete your account
_Draft. The publisher fills in the bracketed fields before this is published at https://liketrek.com/delete-account._
Your TREK account and everything in it live on the TREK server you signed in to, not with [PUBLISHER LEGAL NAME], the publisher of the app. You can delete it in either of these ways:
## In the app
1. Open the app and sign in.
2. Go to **Settings > Account**.
3. Choose **Delete account** and confirm.
The account is deleted on the server at once, together with its trips you own, your files, photos, journal entries and settings. Trips you share with others stay with their other members; your contributions to them are removed or anonymised.
## Without the app
Open your TREK server in a web browser, sign in and go to **Settings > Account > Delete account**. If you cannot sign in any more, ask the administrator of your server to delete the account.
## The public demo
Accounts on demo.liketrek.com cannot be deleted one by one: the demo is shared and is reset to its sample data every hour, which removes everything entered there.
## Data on your device
Removing the app from your phone or tablet removes the server address, your session and the trips kept offline.
## Questions
[CONTACT EMAIL]
@@ -1,24 +0,0 @@
TREK is a travel planner for the trips you take with other people. Plan the days together, see every place on the map, keep bookings, costs and packing lists in one place, and still have all of it with you when the connection drops.
TREK is open source and works with a TREK server: one you run yourself, at home or on a rented server, one your organisation provides, or the public demo to try it out. Your trips stay on that server. The makers of TREK receive none of your data.
PLAN TOGETHER
• Days with places, notes, transports and stays, in order and on the map
• Changes from everyone on the trip appear live
• Roles that decide who may change what
BOOKINGS AND COSTS
• Flights, trains, hotels, rentals and tickets, with their documents
• Shared expenses with fair splits and who owes whom, in any currency
ON THE ROAD
• Trips you choose are kept on the device, maps included, for when you are offline
• Packing lists and to-dos for the whole group
• A travel journal with photos, and an atlas of the countries you have visited
YOUR DATA
• No account with us, no tracking, no ads
• Sign in with a password, a passkey or your organisation's single sign-on
• Available in 27 languages
To use the app you need the address of a TREK server. Without one yet, tap "Try the demo" on the first screen, or read how to run your own at github.com/liketrek/TREK.

Some files were not shown because too many files have changed in this diff Show More