Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cb6149f8ba | |||
| 8963180006 | |||
| 5c37dd43c5 | |||
| 6f3c051c7c | |||
| 99c1a2d6c7 | |||
| a3b04887a1 | |||
| 8b6a65023f | |||
| a5342df648 | |||
| f44b143371 | |||
| d911122a37 | |||
| a69eee191c | |||
| 90f31d3f07 | |||
| d4da05bbc9 | |||
| bd1de9e1d5 | |||
| c33eb5b593 | |||
| dcfbf89234 | |||
| 6b6d971f07 | |||
| 9fc98b5d49 | |||
| daaa6c425d | |||
| b33ef6bec2 | |||
| eb9f6a3438 | |||
| 7823ab1550 | |||
| 47fdd3588c | |||
| a9ebd2d1ac | |||
| 9c32b42256 | |||
| 26611b46e4 | |||
| e3eec8d6d1 | |||
| 2d398568da | |||
| 4051af58a7 | |||
| 3681db6cd4 | |||
| 44cefc2fe2 | |||
| 176bfaefe0 | |||
| 7572c610a6 | |||
| f567e15a99 | |||
| b1479d939a | |||
| 71913eb6e2 | |||
| 601ed66a38 | |||
| 9a69b092e8 | |||
| be211c22ed | |||
| 293b25466e | |||
| 1ba58e8b89 | |||
| ae0d4e4520 | |||
| c5765bbf15 | |||
| 9832d12751 | |||
| 14825df993 | |||
| e12be0a185 | |||
| 9f1f33213b | |||
| b2c029f02a | |||
| 915a1f64e6 |
@@ -0,0 +1,191 @@
|
||||
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"
|
||||
@@ -0,0 +1,132 @@
|
||||
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
|
||||
@@ -182,7 +182,7 @@ Most of what follows is an addon an admin switches on or off. Lists, Costs, Docu
|
||||
|
||||
#### 📱 Mobile and offline
|
||||
|
||||
- **Installable**: iOS and Android straight from the browser, no App Store, standalone with a themed status bar
|
||||
- **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
|
||||
- **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,12 +234,15 @@ the details.
|
||||
## Get started in 30 seconds
|
||||
|
||||
```bash
|
||||
ENCRYPTION_KEY=$(openssl rand -hex 32) docker run -d -p 3000:3000 \
|
||||
ENCRYPTION_KEY=$(openssl rand -hex 32) docker run -d --name trek -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`. 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`).
|
||||
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.
|
||||
|
||||
<div align="center">
|
||||
|
||||
@@ -297,7 +300,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 — no App Store needed.
|
||||
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:
|
||||
|
||||
1. Open TREK in the browser (HTTPS required)
|
||||
2. **iOS**: Share ▸ *Add to Home Screen*
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
name: trek
|
||||
version: 4.3.4
|
||||
version: 5.0.0
|
||||
description: Minimal Helm chart for TREK app
|
||||
appVersion: "4.3.4"
|
||||
appVersion: "5.0.0"
|
||||
|
||||
@@ -7,3 +7,7 @@ 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/
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
# 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
|
||||
@@ -0,0 +1,2 @@
|
||||
/build/*
|
||||
!/build/.npmkeep
|
||||
@@ -0,0 +1,66 @@
|
||||
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'
|
||||
@@ -0,0 +1,25 @@
|
||||
// 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()
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
# 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
|
||||
@@ -0,0 +1,80 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,50 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
@@ -0,0 +1,12 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#09090B</color>
|
||||
<color name="launch_background">#09090B</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,25 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,30 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,28 @@
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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')
|
||||
@@ -0,0 +1,22 @@
|
||||
# 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
|
||||
@@ -0,0 +1,7 @@
|
||||
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
|
||||
@@ -0,0 +1,251 @@
|
||||
#!/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" "$@"
|
||||
@@ -0,0 +1,94 @@
|
||||
@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
|
||||
@@ -0,0 +1,5 @@
|
||||
include ':app'
|
||||
include ':capacitor-cordova-android-plugins'
|
||||
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
|
||||
|
||||
apply from: 'capacitor.settings.gradle'
|
||||
@@ -0,0 +1,18 @@
|
||||
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'
|
||||
}
|
||||
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 164 KiB |
@@ -0,0 +1,60 @@
|
||||
/// <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;
|
||||
@@ -0,0 +1,72 @@
|
||||
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`) });
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -31,6 +31,14 @@ 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',
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# 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.
|
||||
@@ -0,0 +1 @@
|
||||
The first release of TREK for Android: connect to your TREK server, plan together, and take your trips offline.
|
||||
@@ -0,0 +1,24 @@
|
||||
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.
|
||||
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 54 KiB |
@@ -0,0 +1 @@
|
||||
Plan trips together on your own TREK server, with maps, bookings and costs.
|
||||
@@ -0,0 +1 @@
|
||||
TREK Travel Planner
|
||||
@@ -0,0 +1 @@
|
||||
2026 [PUBLISHER LEGAL NAME]
|
||||
@@ -0,0 +1,29 @@
|
||||
# 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]
|
||||
@@ -0,0 +1,24 @@
|
||||
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.
|
||||