diff --git a/client/android/.gitignore b/client/android/.gitignore index 48354a3df..511927dea 100644 --- a/client/android/.gitignore +++ b/client/android/.gitignore @@ -62,7 +62,13 @@ captures/ .cxx/ # Google Services (e.g. APIs or Firebase) -# google-services.json +# Deliberately ignored: this file pins the build to one Firebase project. +# Committing it would make every fork's builds register against the upstream +# FCM project, and rotating the project would become a repo change. Official +# builds inject it from a CI secret; see google-services.json.example. +# app/build.gradle already tolerates its absence (push is simply inert). +google-services.json +app/google-services.json # Freeline freeline.py diff --git a/client/android/app/capacitor.build.gradle b/client/android/app/capacitor.build.gradle index bbfb44fad..262141409 100644 --- a/client/android/app/capacitor.build.gradle +++ b/client/android/app/capacitor.build.gradle @@ -9,7 +9,24 @@ android { apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" dependencies { - + implementation project(':capacitor-app') + implementation project(':capacitor-browser') + implementation project(':capacitor-calendar') + implementation project(':capacitor-camera') + implementation project(':capacitor-file-transfer') + implementation project(':capacitor-filesystem') + implementation project(':capacitor-geolocation') + implementation project(':capacitor-haptics') + implementation project(':capacitor-inappbrowser') + implementation project(':capacitor-keyboard') + implementation project(':capacitor-local-notifications') + implementation project(':capacitor-network') + implementation project(':capacitor-preferences') + implementation project(':capacitor-push-notifications') + implementation project(':capacitor-share') + implementation project(':capacitor-splash-screen') + implementation project(':capacitor-status-bar') + implementation project(':capacitor-toast') } diff --git a/client/android/app/src/main/AndroidManifest.xml b/client/android/app/src/main/AndroidManifest.xml index b06ddbfd8..68156fea0 100644 --- a/client/android/app/src/main/AndroidManifest.xml +++ b/client/android/app/src/main/AndroidManifest.xml @@ -1,10 +1,12 @@ - + @@ -22,6 +24,29 @@ + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + diff --git a/client/android/app/src/main/res/drawable-hdpi/notification_icon.png b/client/android/app/src/main/res/drawable-hdpi/notification_icon.png new file mode 100644 index 000000000..c9c491d54 Binary files /dev/null and b/client/android/app/src/main/res/drawable-hdpi/notification_icon.png differ diff --git a/client/android/app/src/main/res/drawable-mdpi/notification_icon.png b/client/android/app/src/main/res/drawable-mdpi/notification_icon.png new file mode 100644 index 000000000..e19ce026c Binary files /dev/null and b/client/android/app/src/main/res/drawable-mdpi/notification_icon.png differ diff --git a/client/android/app/src/main/res/drawable-xhdpi/notification_icon.png b/client/android/app/src/main/res/drawable-xhdpi/notification_icon.png new file mode 100644 index 000000000..9e5e54242 Binary files /dev/null and b/client/android/app/src/main/res/drawable-xhdpi/notification_icon.png differ diff --git a/client/android/app/src/main/res/drawable-xxhdpi/notification_icon.png b/client/android/app/src/main/res/drawable-xxhdpi/notification_icon.png new file mode 100644 index 000000000..b4240d166 Binary files /dev/null and b/client/android/app/src/main/res/drawable-xxhdpi/notification_icon.png differ diff --git a/client/android/app/src/main/res/drawable-xxxhdpi/notification_icon.png b/client/android/app/src/main/res/drawable-xxxhdpi/notification_icon.png new file mode 100644 index 000000000..1a36bfff7 Binary files /dev/null and b/client/android/app/src/main/res/drawable-xxxhdpi/notification_icon.png differ diff --git a/client/android/app/src/main/res/values/strings.xml b/client/android/app/src/main/res/values/strings.xml index 0b80e5533..ba2fc8823 100644 --- a/client/android/app/src/main/res/values/strings.xml +++ b/client/android/app/src/main/res/values/strings.xml @@ -4,4 +4,8 @@ TREK - Plan your trips! com.liketrek.trek com.liketrek.trek + + trek_default diff --git a/client/android/app/src/main/res/xml/network_security_config.xml b/client/android/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 000000000..479404e90 --- /dev/null +++ b/client/android/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/client/android/capacitor.settings.gradle b/client/android/capacitor.settings.gradle index 874b109a3..c22006c5e 100644 --- a/client/android/capacitor.settings.gradle +++ b/client/android/capacitor.settings.gradle @@ -1,3 +1,57 @@ // 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-browser' +project(':capacitor-browser').projectDir = new File('../../node_modules/@capacitor/browser/android') + +include ':capacitor-calendar' +project(':capacitor-calendar').projectDir = new File('../../node_modules/@capacitor/calendar/android') + +include ':capacitor-camera' +project(':capacitor-camera').projectDir = new File('../../node_modules/@capacitor/camera/android') + +include ':capacitor-file-transfer' +project(':capacitor-file-transfer').projectDir = new File('../../node_modules/@capacitor/file-transfer/android') + +include ':capacitor-filesystem' +project(':capacitor-filesystem').projectDir = new File('../../node_modules/@capacitor/filesystem/android') + +include ':capacitor-geolocation' +project(':capacitor-geolocation').projectDir = new File('../../node_modules/@capacitor/geolocation/android') + +include ':capacitor-haptics' +project(':capacitor-haptics').projectDir = new File('../../node_modules/@capacitor/haptics/android') + +include ':capacitor-inappbrowser' +project(':capacitor-inappbrowser').projectDir = new File('../../node_modules/@capacitor/inappbrowser/android') + +include ':capacitor-keyboard' +project(':capacitor-keyboard').projectDir = new File('../../node_modules/@capacitor/keyboard/android') + +include ':capacitor-local-notifications' +project(':capacitor-local-notifications').projectDir = new File('../../node_modules/@capacitor/local-notifications/android') + +include ':capacitor-network' +project(':capacitor-network').projectDir = new File('../../node_modules/@capacitor/network/android') + +include ':capacitor-preferences' +project(':capacitor-preferences').projectDir = new File('../../node_modules/@capacitor/preferences/android') + +include ':capacitor-push-notifications' +project(':capacitor-push-notifications').projectDir = new File('../../node_modules/@capacitor/push-notifications/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') + +include ':capacitor-toast' +project(':capacitor-toast').projectDir = new File('../../node_modules/@capacitor/toast/android') diff --git a/client/android/google-services.json.example b/client/android/google-services.json.example new file mode 100644 index 000000000..5c4e9b003 --- /dev/null +++ b/client/android/google-services.json.example @@ -0,0 +1,44 @@ +{ + "_comment": [ + "Template only — this file is not read by the build.", + "", + "To enable Android push notifications:", + " 1. Create a Firebase project at https://console.firebase.google.com", + " 2. Add an Android app with package name com.liketrek.trek", + " 3. Download the generated google-services.json", + " 4. Place it at client/android/app/google-services.json (gitignored)", + "", + "Without it, app/build.gradle skips the google-services plugin and the app", + "still builds and runs — PushNotifications.register() simply fails at runtime.", + "", + "The server that SENDS the notifications needs a service-account key from the", + "SAME Firebase project. See docs for the self-hosting implications." + ], + "project_info": { + "project_number": "000000000000", + "project_id": "your-firebase-project-id", + "storage_bucket": "your-firebase-project-id.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:000000000000:android:0000000000000000000000", + "android_client_info": { + "package_name": "com.liketrek.trek" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "REPLACE_WITH_YOUR_ANDROID_API_KEY" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} diff --git a/client/android/variables.gradle b/client/android/variables.gradle index ee4ba41c4..199310493 100644 --- a/client/android/variables.gradle +++ b/client/android/variables.gradle @@ -1,5 +1,10 @@ ext { - minSdkVersion = 24 + // 26, not Capacitor's default 24: @capacitor/inappbrowser requires a minimum + // target of 26. This drops Android 7.0/7.1 (~1% of devices and no longer + // receiving security updates). Removing @capacitor/inappbrowser — its system- + // browser mode overlaps @capacitor/browser almost exactly — would allow 24 + // again. + minSdkVersion = 26 compileSdkVersion = 36 targetSdkVersion = 36 androidxActivityVersion = '1.11.0' @@ -13,4 +18,7 @@ ext { androidxJunitVersion = '1.3.0' androidxEspressoCoreVersion = '3.7.0' cordovaAndroidVersion = '14.0.1' + // Consumed by @capacitor/push-notifications. Pinned rather than left to the + // plugin's default so an FCM SDK bump is a visible, reviewable change here. + firebaseMessagingVersion = '25.0.1' } \ No newline at end of file diff --git a/client/capacitor.config.ts b/client/capacitor.config.ts index 39cba7a26..4e4e6ca88 100644 --- a/client/capacitor.config.ts +++ b/client/capacitor.config.ts @@ -1,9 +1,67 @@ +/// +/// +/// +/// import type { CapacitorConfig } from '@capacitor/cli'; +import { KeyboardResize } from '@capacitor/keyboard'; const config: CapacitorConfig = { appId: 'com.liketrek.trek', appName: 'TREK - Plan your trips!', - webDir: 'dist' + webDir: 'dist', + // TREK is self-hosted: the server the app talks to is whatever the user + // enters, so every request is cross-origin. Both schemes stay https so the + // webview origin is a secure context (crypto.subtle, WebAuthn, service + // workers, IndexedDB persistence all require it). + server: { + androidScheme: 'https', + iosScheme: 'https', + }, + plugins: { + PushNotifications: { + // No `badge`: TREK has no unread count the OS could keep honest, and a + // badge that only ever grows is worse than none. + presentationOptions: ['sound', 'banner', 'list'], + }, + LocalNotifications: { + // Same white-on-transparent mask as FCM uses; generated by + // scripts/generate-native-icons.mjs. Without it Android falls back to the + // launcher icon and renders a white blob. + smallIcon: 'notification_icon', + presentationOptions: ['sound', 'banner', 'list'], + }, + SplashScreen: { + // Matches 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', + launchShowDuration: 500, + // Deliberately true. With `false` the splash stays up until JS calls + // SplashScreen.hide(), and until that call exists a failure to boot the + // bundle looks like a frozen app. Flip this to false — and drop + // launchShowDuration — in the same change that adds the hide() call once + // the React tree has mounted. + launchAutoHide: true, + 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, + // No `style`: TREK has a real appearance system, so the keyboard should + // follow the device/app theme rather than being pinned here. + }, + // No StatusBar block on purpose. From Android 15 / Capacitor 8 the app is + // always edge-to-edge, so `overlaysWebView` and `backgroundColor` no longer + // do anything. The status-bar style has to follow the user's chosen scheme + // at runtime — call StatusBar.setStyle() from applyAppearance(). + }, }; export default config; diff --git a/client/eslint.config.mjs b/client/eslint.config.mjs index f007649a9..fe8e629c1 100644 --- a/client/eslint.config.mjs +++ b/client/eslint.config.mjs @@ -30,6 +30,14 @@ export default tseslint.config( 'public', 'test-results', 'playwright-report', + // Capacitor native shells. `npx cap sync` copies the built bundle from + // dist/ into both platforms, so without these ESLint lints the minified + // output and `npm run lint` reports tens of thousands of errors that have + // nothing to do with the source. 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. + 'android/**', + 'ios/**', 'e2e/**', 'scripts/**', '**/*.config.js', diff --git a/client/ios/App/App.xcodeproj/project.pbxproj b/client/ios/App/App.xcodeproj/project.pbxproj index c18597379..45991c4af 100644 --- a/client/ios/App/App.xcodeproj/project.pbxproj +++ b/client/ios/App/App.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ 504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; }; 50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; }; 9582B6832FE993A70072D4E8 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9582B6822FE993A50072D4E8 /* SceneDelegate.swift */; }; + 7A1E00032C0000000000A003 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 7A1E00022C0000000000A002 /* PrivacyInfo.xcprivacy */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -30,6 +31,8 @@ 50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = ""; }; 9582B6822FE993A50072D4E8 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 958DCC722DB07C7200EA8C5F /* debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = debug.xcconfig; path = ../debug.xcconfig; sourceTree = SOURCE_ROOT; }; + 7A1E00012C0000000000A001 /* App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = App.entitlements; sourceTree = ""; }; + 7A1E00022C0000000000A002 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -71,6 +74,8 @@ 504EC30E1FED79650016851F /* Assets.xcassets */, 504EC3101FED79650016851F /* LaunchScreen.storyboard */, 504EC3131FED79650016851F /* Info.plist */, + 7A1E00012C0000000000A001 /* App.entitlements */, + 7A1E00022C0000000000A002 /* PrivacyInfo.xcprivacy */, 2FAD9762203C412B000D30F8 /* config.xml */, 50B271D01FEDC1A000F3C39B /* public */, ); @@ -113,6 +118,11 @@ CreatedOnToolsVersion = 9.2; LastSwiftMigration = 1100; ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Push = { + enabled = 1; + }; + }; }; }; }; @@ -148,6 +158,7 @@ 50379B232058CBB4000EE86E /* capacitor.config.json in Resources */, 504EC30D1FED79650016851F /* Main.storyboard in Resources */, 2FAD9763203C412B000D30F8 /* config.xml in Resources */, + 7A1E00032C0000000000A003 /* PrivacyInfo.xcprivacy in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -299,6 +310,7 @@ baseConfigurationReference = 958DCC722DB07C7200EA8C5F /* debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = App/App.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; INFOPLIST_FILE = App/Info.plist; @@ -321,6 +333,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = App/App.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; INFOPLIST_FILE = App/Info.plist; diff --git a/client/ios/App/App/App.entitlements b/client/ios/App/App/App.entitlements new file mode 100644 index 000000000..78c3cf2ae --- /dev/null +++ b/client/ios/App/App/App.entitlements @@ -0,0 +1,21 @@ + + + + + + aps-environment + development + + diff --git a/client/ios/App/App/AppDelegate.swift b/client/ios/App/App/AppDelegate.swift index fec95acae..cc7ffaae1 100644 --- a/client/ios/App/App/AppDelegate.swift +++ b/client/ios/App/App/AppDelegate.swift @@ -33,6 +33,22 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. } + // MARK: - Push notifications + // + // @capacitor/push-notifications does not swizzle the APNs callbacks; it listens + // on NotificationCenter instead, so these two forwarders are mandatory. Without + // them PushNotifications.register() resolves and then nothing ever arrives — + // neither the `registration` nor the `registrationError` listener fires, which + // makes it look like a server problem rather than a missing 6 lines here. + + func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + NotificationCenter.default.post(name: .capacitorDidRegisterForRemoteNotifications, object: deviceToken) + } + + func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { + NotificationCenter.default.post(name: .capacitorDidFailToRegisterForRemoteNotifications, object: error) + } + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { diff --git a/client/ios/App/App/Info.plist b/client/ios/App/App/Info.plist index 2004e80eb..fc2f05c0c 100644 --- a/client/ios/App/App/Info.plist +++ b/client/ios/App/App/Info.plist @@ -24,6 +24,84 @@ $(CURRENT_PROJECT_VERSION) LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + + NSLocalNetworkUsageDescription + TREK connects to the TREK server you choose, which is often running on your home or office network. + + NSCameraUsageDescription + TREK uses the camera so you can photograph a place, a journey entry or a booking document and add it to your trip. + NSPhotoLibraryUsageDescription + TREK needs access to your photos so you can add them to trips, places and journey entries. + NSPhotoLibraryAddUsageDescription + TREK saves photos and exports you download from your trips back to your photo library. + + NSLocationWhenInUseUsageDescription + TREK uses your location to centre the map on you and to find places near you. + NSLocationAlwaysAndWhenInUseUsageDescription + TREK uses your location to centre the map on you and to find places near you. + + NSCalendarsWriteOnlyAccessUsageDescription + TREK adds your trip days and bookings to your calendar. + NSCalendarsFullAccessUsageDescription + TREK reads your calendar to show existing commitments alongside your trip, and adds trip events to it. + NSCalendarsUsageDescription + TREK adds your trip days and bookings to your calendar. + + CFBundleURLTypes + + + CFBundleURLName + com.liketrek.trek + CFBundleTypeRole + Editor + CFBundleURLSchemes + + com.liketrek.trek + + + UIApplicationSceneManifest UIApplicationSupportsMultipleScenes diff --git a/client/ios/App/App/PrivacyInfo.xcprivacy b/client/ios/App/App/PrivacyInfo.xcprivacy new file mode 100644 index 000000000..4ec3e4602 --- /dev/null +++ b/client/ios/App/App/PrivacyInfo.xcprivacy @@ -0,0 +1,47 @@ + + + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPITypeReasons + + C617.1 + + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + CA92.1 + + + + + NSPrivacyTracking + + NSPrivacyTrackingDomains + + NSPrivacyCollectedDataTypes + + + diff --git a/client/ios/App/CapApp-SPM/Package.swift b/client/ios/App/CapApp-SPM/Package.swift index 856517daa..6b3c3b311 100644 --- a/client/ios/App/CapApp-SPM/Package.swift +++ b/client/ios/App/CapApp-SPM/Package.swift @@ -11,14 +11,50 @@ let package = Package( targets: ["CapApp-SPM"]) ], dependencies: [ - .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", exact: "8.5.0") + .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", exact: "8.5.0"), + .package(name: "CapacitorApp", path: "../../../../node_modules/@capacitor/app"), + .package(name: "CapacitorBrowser", path: "../../../../node_modules/@capacitor/browser"), + .package(name: "CapacitorCalendar", path: "../../../../node_modules/@capacitor/calendar"), + .package(name: "CapacitorCamera", path: "../../../../node_modules/@capacitor/camera"), + .package(name: "CapacitorFileTransfer", path: "../../../../node_modules/@capacitor/file-transfer"), + .package(name: "CapacitorFilesystem", path: "../../../../node_modules/@capacitor/filesystem"), + .package(name: "CapacitorGeolocation", path: "../../../../node_modules/@capacitor/geolocation"), + .package(name: "CapacitorHaptics", path: "../../../../node_modules/@capacitor/haptics"), + .package(name: "CapacitorInappbrowser", path: "../../../../node_modules/@capacitor/inappbrowser"), + .package(name: "CapacitorKeyboard", path: "../../../../node_modules/@capacitor/keyboard"), + .package(name: "CapacitorLocalNotifications", path: "../../../../node_modules/@capacitor/local-notifications"), + .package(name: "CapacitorNetwork", path: "../../../../node_modules/@capacitor/network"), + .package(name: "CapacitorPreferences", path: "../../../../node_modules/@capacitor/preferences"), + .package(name: "CapacitorPushNotifications", path: "../../../../node_modules/@capacitor/push-notifications"), + .package(name: "CapacitorShare", path: "../../../../node_modules/@capacitor/share"), + .package(name: "CapacitorSplashScreen", path: "../../../../node_modules/@capacitor/splash-screen"), + .package(name: "CapacitorStatusBar", path: "../../../../node_modules/@capacitor/status-bar"), + .package(name: "CapacitorToast", path: "../../../../node_modules/@capacitor/toast") ], targets: [ .target( name: "CapApp-SPM", dependencies: [ .product(name: "Capacitor", package: "capacitor-swift-pm"), - .product(name: "Cordova", package: "capacitor-swift-pm") + .product(name: "Cordova", package: "capacitor-swift-pm"), + .product(name: "CapacitorApp", package: "CapacitorApp"), + .product(name: "CapacitorBrowser", package: "CapacitorBrowser"), + .product(name: "CapacitorCalendar", package: "CapacitorCalendar"), + .product(name: "CapacitorCamera", package: "CapacitorCamera"), + .product(name: "CapacitorFileTransfer", package: "CapacitorFileTransfer"), + .product(name: "CapacitorFilesystem", package: "CapacitorFilesystem"), + .product(name: "CapacitorGeolocation", package: "CapacitorGeolocation"), + .product(name: "CapacitorHaptics", package: "CapacitorHaptics"), + .product(name: "CapacitorInappbrowser", package: "CapacitorInappbrowser"), + .product(name: "CapacitorKeyboard", package: "CapacitorKeyboard"), + .product(name: "CapacitorLocalNotifications", package: "CapacitorLocalNotifications"), + .product(name: "CapacitorNetwork", package: "CapacitorNetwork"), + .product(name: "CapacitorPreferences", package: "CapacitorPreferences"), + .product(name: "CapacitorPushNotifications", package: "CapacitorPushNotifications"), + .product(name: "CapacitorShare", package: "CapacitorShare"), + .product(name: "CapacitorSplashScreen", package: "CapacitorSplashScreen"), + .product(name: "CapacitorStatusBar", package: "CapacitorStatusBar"), + .product(name: "CapacitorToast", package: "CapacitorToast") ] ) ] diff --git a/client/package.json b/client/package.json index 9ddf59b88..6e091f288 100644 --- a/client/package.json +++ b/client/package.json @@ -6,6 +6,7 @@ "scripts": { "dev": "vite", "prebuild": "node scripts/generate-icons.mjs", + "icons:native": "node scripts/generate-native-icons.mjs", "build": "vite build", "build:analyze": "vite build --mode analyze", "preview": "vite preview", @@ -30,8 +31,26 @@ }, "dependencies": { "@capacitor/android": "^8.5.0", + "@capacitor/app": "^8.1.1", + "@capacitor/browser": "^8.0.4", + "@capacitor/calendar": "^1.0.1", + "@capacitor/camera": "^8.2.3", "@capacitor/core": "^8.5.0", + "@capacitor/file-transfer": "^2.0.5", + "@capacitor/filesystem": "^8.1.3", + "@capacitor/geolocation": "^8.2.2", + "@capacitor/haptics": "^8.0.2", + "@capacitor/inappbrowser": "^4.0.3", "@capacitor/ios": "^8.5.0", + "@capacitor/keyboard": "^8.0.5", + "@capacitor/local-notifications": "^8.3.1", + "@capacitor/network": "^8.0.1", + "@capacitor/preferences": "^8.0.1", + "@capacitor/push-notifications": "^8.1.2", + "@capacitor/share": "^8.0.1", + "@capacitor/splash-screen": "^8.0.2", + "@capacitor/status-bar": "^8.0.3", + "@capacitor/toast": "^8.0.1", "@fontsource/bebas-neue": "^5.3.0", "@fontsource/eb-garamond": "^5.3.0", "@fontsource/geist-sans": "^5.2.5", diff --git a/client/scripts/generate-native-icons.mjs b/client/scripts/generate-native-icons.mjs new file mode 100644 index 000000000..6cdecb956 --- /dev/null +++ b/client/scripts/generate-native-icons.mjs @@ -0,0 +1,67 @@ +/** + * Generates the Android status-bar notification icon from the master + * white-silhouette SVG. + * + * Android renders the small notification icon as a mask: every non-transparent + * pixel becomes the tint colour and everything else is dropped. Feeding it the + * launcher icon (a full-bleed coloured square) therefore produces a solid white + * blob, which is the single most common Capacitor push-notification complaint. + * public/icons/icon-white.svg is already white-on-transparent, so we only have + * to rasterise it at the five densities and flatten the colour to pure white so + * any future edit to that SVG's fill cannot silently change the mask. + * + * Referenced from AndroidManifest.xml as @drawable/notification_icon. + * Run: node scripts/generate-native-icons.mjs + */ +import { mkdirSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; +import sharp from 'sharp'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const clientDir = join(__dirname, '..'); +const source = join(clientDir, 'public', 'icons', 'icon-white.svg'); +const resDir = join(clientDir, 'android', 'app', 'src', 'main', 'res'); + +// Android notification icon sizes, in px, per density bucket (24dp baseline). +const densities = [ + { dir: 'drawable-mdpi', size: 24 }, + { dir: 'drawable-hdpi', size: 36 }, + { dir: 'drawable-xhdpi', size: 48 }, + { dir: 'drawable-xxhdpi', size: 72 }, + { dir: 'drawable-xxxhdpi', size: 96 }, +]; + +for (const { dir, size } of densities) { + const outDir = join(resDir, dir); + mkdirSync(outDir, { recursive: true }); + + // Inset the glyph slightly: Android's status bar clips tight to the 24dp box + // and a mark that touches the edge reads as cropped. + const glyph = Math.round(size * 0.86); + + const mask = await sharp(source, { density: 512 }) + .resize(glyph, glyph, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }) + .extend({ + top: Math.floor((size - glyph) / 2), + bottom: Math.ceil((size - glyph) / 2), + left: Math.floor((size - glyph) / 2), + right: Math.ceil((size - glyph) / 2), + background: { r: 0, g: 0, b: 0, alpha: 0 }, + }) + .ensureAlpha() + .toBuffer(); + + // Keep the alpha, force every visible pixel to pure white. `dest-in` composites + // the white plate through the glyph's alpha channel. + await sharp({ + create: { width: size, height: size, channels: 4, background: { r: 255, g: 255, b: 255, alpha: 1 } }, + }) + .composite([{ input: mask, blend: 'dest-in' }]) + .png({ compressionLevel: 9 }) + .toFile(join(outDir, 'notification_icon.png')); + + console.log(` ✓ ${dir}/notification_icon.png (${size}x${size})`); +} + +console.log('Android notification icons generated.'); diff --git a/package-lock.json b/package-lock.json index 2cd6933a8..bd22295da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,8 +28,26 @@ "version": "3.4.1", "dependencies": { "@capacitor/android": "^8.5.0", + "@capacitor/app": "^8.1.1", + "@capacitor/browser": "^8.0.4", + "@capacitor/calendar": "^1.0.1", + "@capacitor/camera": "^8.2.3", "@capacitor/core": "^8.5.0", + "@capacitor/file-transfer": "^2.0.5", + "@capacitor/filesystem": "^8.1.3", + "@capacitor/geolocation": "^8.2.2", + "@capacitor/haptics": "^8.0.2", + "@capacitor/inappbrowser": "^4.0.3", "@capacitor/ios": "^8.5.0", + "@capacitor/keyboard": "^8.0.5", + "@capacitor/local-notifications": "^8.3.1", + "@capacitor/network": "^8.0.1", + "@capacitor/preferences": "^8.0.1", + "@capacitor/push-notifications": "^8.1.2", + "@capacitor/share": "^8.0.1", + "@capacitor/splash-screen": "^8.0.2", + "@capacitor/status-bar": "^8.0.3", + "@capacitor/toast": "^8.0.1", "@fontsource/bebas-neue": "^5.3.0", "@fontsource/eb-garamond": "^5.3.0", "@fontsource/geist-sans": "^5.2.5", @@ -2379,6 +2397,42 @@ "@capacitor/core": "^8.5.0" } }, + "node_modules/@capacitor/app": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@capacitor/app/-/app-8.1.1.tgz", + "integrity": "sha512-xM2ZTX5jK60tFtmjsmJv+Cdj1Qsb6NcavkqmdEnCPQBeya9oKhamZJxYOnQNj1ZvcJM7sWfG6BEtSLx42pisbA==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, + "node_modules/@capacitor/browser": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@capacitor/browser/-/browser-8.0.4.tgz", + "integrity": "sha512-ORZ4u/y+7aMuu6yVuk6SP529fUZyqcbnuHB1q5McpA3o2SYfVoi28iB8rsQjI2ad1qlKJd29ZUuGtspXLBDlWg==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, + "node_modules/@capacitor/calendar": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@capacitor/calendar/-/calendar-1.0.1.tgz", + "integrity": "sha512-2ogW+Jlamy6BJkZmaggB/Jx282G+o3aBPfgx3GxmjlLjl8hqcorQKFhlCiveDui6xEKoVy13AQpKhukHQ8MrCQ==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, + "node_modules/@capacitor/camera": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/@capacitor/camera/-/camera-8.2.3.tgz", + "integrity": "sha512-x9Pl5UgvrZyU5Hl53Fly9f6u019hhgN+gSdDgi+WygxK9DUVuYKjigplOXz3VJQ9m8ttUSjqN/rAC3JVnyEzcA==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, "node_modules/@capacitor/cli": { "version": "8.5.0", "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-8.5.0.tgz", @@ -2503,6 +2557,60 @@ "tslib": "^2.1.0" } }, + "node_modules/@capacitor/file-transfer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@capacitor/file-transfer/-/file-transfer-2.0.5.tgz", + "integrity": "sha512-G2a/+NKvADNcmxMSqy6HPp+Ij3trCy0+EVNMrRWmp8ueyCNaJEi96LpfEIztv+vqK9+xLRskQ7iVl+Pj8qt4hg==", + "license": "MIT", + "dependencies": { + "@capacitor/synapse": "^1.0.4" + }, + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, + "node_modules/@capacitor/filesystem": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/@capacitor/filesystem/-/filesystem-8.1.3.tgz", + "integrity": "sha512-GNBlRkaXEEb+6WP/YsDXr2zygyOHoieuye6viyd2ZkEdBwfseG41jxfarVxLWH0r0owhyNUbwJ/Qi8fubqr6wA==", + "license": "MIT", + "dependencies": { + "@capacitor/synapse": "^1.0.4" + }, + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, + "node_modules/@capacitor/geolocation": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@capacitor/geolocation/-/geolocation-8.2.2.tgz", + "integrity": "sha512-X8ryw6CfL4LspJnnfVL4++DUKKZVfdUIA9JljWVRljEDIQxZQZtz6xf7qstht+ymc+le6oa5EaUChVA6KsCYlA==", + "license": "MIT", + "dependencies": { + "@capacitor/synapse": "^1.0.4" + }, + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, + "node_modules/@capacitor/haptics": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-8.0.2.tgz", + "integrity": "sha512-c2hZzRR5Fk1tbTvhG1jhh2XBAf3EhnIerMIb2sl7Mt41Gxx1fhBJFDa0/BI1IbY4loVepyyuqNC9820/GZuoWQ==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, + "node_modules/@capacitor/inappbrowser": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@capacitor/inappbrowser/-/inappbrowser-4.0.3.tgz", + "integrity": "sha512-Jnl6Ssp2QZByncO6QhDKzNpi3yeum1aSQM+70VMVh8TJ16HOwOrcTnf8E4+7FpT9ruSRGyhqQu1yBSrpl+wD1g==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, "node_modules/@capacitor/ios": { "version": "8.5.0", "resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-8.5.0.tgz", @@ -2512,6 +2620,93 @@ "@capacitor/core": "^8.5.0" } }, + "node_modules/@capacitor/keyboard": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-8.0.5.tgz", + "integrity": "sha512-oFXygC4eKYA5l2MdpTR06L2M/4x6e2SLD5yS1T9+UBDKTkzyvhWKEhbYLUaTIBPpLKqlfGudJw1X73S1H9eUzQ==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, + "node_modules/@capacitor/local-notifications": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@capacitor/local-notifications/-/local-notifications-8.3.1.tgz", + "integrity": "sha512-F7zu/+p/mIR958YP4axOkeI5hBKRaXDyYjbNPrAil2wWfd7OxScPSTvP9t85NpQ0eULVRu0/sntoxOg6ck477A==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, + "node_modules/@capacitor/network": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@capacitor/network/-/network-8.0.1.tgz", + "integrity": "sha512-9xK/FHFmzKGanB6BdoSZOzXk8vF0OFVQSQ4PAsIrzAzLuXHryO317qy8dcHVpgxYeuZq2noI0My9z1DvVDi/9w==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, + "node_modules/@capacitor/preferences": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@capacitor/preferences/-/preferences-8.0.1.tgz", + "integrity": "sha512-T6no3ebi79XJCk91U3Jp/liJUwgBdvHR+s6vhvPkPxSuch7z3zx5Rv1bdWM6sWruNx+pViuEGqZvbfCdyBvcHQ==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, + "node_modules/@capacitor/push-notifications": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@capacitor/push-notifications/-/push-notifications-8.1.2.tgz", + "integrity": "sha512-TQl4XxqJfNF+KbSYBFMXYYT5WXUaleVKXlByhh/8+KySMVNjp+l5OqAUoGe/sY1IAnGXEuUeCjA/WLZEVvGwrA==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, + "node_modules/@capacitor/share": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@capacitor/share/-/share-8.0.1.tgz", + "integrity": "sha512-3cSBKBCJVon54rKDROP2rqGyeGks4pBh9TbaEk9S375Kbek/ZHe72N50zIa0Vn9Eac/SuhwgehO/mmA4CsUOiw==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, + "node_modules/@capacitor/splash-screen": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@capacitor/splash-screen/-/splash-screen-8.0.2.tgz", + "integrity": "sha512-0C6rYScr13WfAE9nPl4ScOQynmjFv9NT3Th+RZkD4ezYHmER6mcl1/lDYsv7jg3Rj3FOvLrlTlmNkUrr83kZzQ==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, + "node_modules/@capacitor/status-bar": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-8.0.3.tgz", + "integrity": "sha512-csSpfNeN49Hx9JaQBSJEIiEbOLtXg3kcc2IpScq2fu5L520h3AWEvsxoH8Srk1jxfRKepaJ4S4sqSC3foI4AgA==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, + "node_modules/@capacitor/synapse": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@capacitor/synapse/-/synapse-1.0.4.tgz", + "integrity": "sha512-/C1FUo8/OkKuAT4nCIu/34ny9siNHr9qtFezu4kxm6GY1wNFxrCFWjfYx5C1tUhVGz3fxBABegupkpjXvjCHrw==", + "license": "ISC" + }, + "node_modules/@capacitor/toast": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@capacitor/toast/-/toast-8.0.1.tgz", + "integrity": "sha512-0uoyftoAeFjtOMiozBo7YXEo+zcpQCAxtBONSvrljJ05SKdlb1A8OFHaV/DZfIpGt+19kzUGaEaUemLdll4wUw==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, "node_modules/@csstools/color-helpers": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz",