Compare commits
62 Commits
edit-proj-
...
fix/ios-sh
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85ac6632ed | ||
|
|
0c91b687f9 | ||
|
|
7093f38e09 | ||
|
|
cb748f9105 | ||
|
|
a3e9a0104d | ||
|
|
981f8b77d0 | ||
|
|
125406a54c | ||
|
|
c6d5876da3 | ||
|
|
0a70914d0c | ||
|
|
b48ab3dc42 | ||
|
|
43328e9d4b | ||
|
|
02e6e3427d | ||
|
|
337a8f7536 | ||
|
|
4978e93711 | ||
|
|
9941264022 | ||
|
|
256018d30d | ||
|
|
c1a5bae5c8 | ||
|
|
c9061e669e | ||
|
|
7b1fec779b | ||
|
|
d1106d9aec | ||
|
|
6f7be2e3b2 | ||
|
|
4fc30562fb | ||
|
|
6afe40bc23 | ||
|
|
402bd2681f | ||
|
|
498a4926bf | ||
|
|
f0ca49b5dc | ||
|
|
07463246f0 | ||
|
|
79ceebbd1d | ||
|
|
ddbd07f315 | ||
|
|
35a6a6bfb3 | ||
|
|
08a55202f5 | ||
| ec41dd52d5 | |||
| 463db39a6b | |||
| fe97dff752 | |||
|
|
903047f13b | ||
|
|
48be234af4 | ||
| 6c0907d905 | |||
|
|
8d8bcf2a7e | ||
| a4b47904c8 | |||
|
|
bb890baacf | ||
| dae23300fe | |||
| 9e401febea | |||
| cd4b279703 | |||
| a3a2d97b9a | |||
| 802050259c | |||
| efd7d50a84 | |||
| 39c389cda8 | |||
| 93fdcaf7ff | |||
| ad419efa0d | |||
| ce45ddb2bd | |||
| 7d306bd204 | |||
| 9713313a40 | |||
|
|
ffa7bac319 | ||
| e0e0a0a183 | |||
| ea662f4430 | |||
| 81647e1f3c | |||
| bf1ee78025 | |||
|
|
66b7d0f46e | ||
|
|
63dcf44125 | ||
| cf1ecdfb4c | |||
| e9ad61b780 | |||
| ad8df3eb93 |
@@ -1 +1 @@
|
|||||||
18.19.0
|
20.18.1
|
||||||
|
|||||||
14
BUILDING.md
14
BUILDING.md
@@ -333,11 +333,11 @@ The `serve` functionality provides a local HTTP server for testing production bu
|
|||||||
- If there are DB changes: before updating the test server, open browser(s) with
|
- If there are DB changes: before updating the test server, open browser(s) with
|
||||||
current version to test DB migrations.
|
current version to test DB migrations.
|
||||||
|
|
||||||
- Update the ClickUp tasks & CHANGELOG.md & the version in package.json, run
|
- Update the ClickUp tasks & CHANGELOG.md & the version in package.json, run:
|
||||||
`npm install`.
|
`npm install`.
|
||||||
|
|
||||||
- Run a build to make sure package-lock version is updated, linting works, etc:
|
- Run a build to make sure linting works, etc:
|
||||||
`npm install && npm run build:web`
|
`npm run build:web`
|
||||||
|
|
||||||
- Commit everything (since the commit hash is used the app).
|
- Commit everything (since the commit hash is used the app).
|
||||||
|
|
||||||
@@ -346,7 +346,7 @@ current version to test DB migrations.
|
|||||||
|
|
||||||
- Tag with the new version,
|
- Tag with the new version,
|
||||||
[online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or
|
[online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or
|
||||||
`git tag 1.0.2 && git push origin 1.0.2`.
|
`git tag 1.3.13 && git push origin 1.3.13`.
|
||||||
|
|
||||||
- For test, build the app:
|
- For test, build the app:
|
||||||
|
|
||||||
@@ -1140,7 +1140,7 @@ export GEM_PATH=$shortened_path
|
|||||||
##### 1. Bump the version in package.json & CHANGELOG.md for `MARKETING_VERSION`, then `grep CURRENT_PROJECT_VERSION ios/App/App.xcodeproj/project.pbxproj` and add 1 for the numbered version here:
|
##### 1. Bump the version in package.json & CHANGELOG.md for `MARKETING_VERSION`, then `grep CURRENT_PROJECT_VERSION ios/App/App.xcodeproj/project.pbxproj` and add 1 for the numbered version here:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ios/App && xcrun agvtool new-version 67 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.3.12;/g" App.xcodeproj/project.pbxproj && cd -
|
cd ios/App && xcrun agvtool new-version 69 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.4.3;/g" App.xcodeproj/project.pbxproj && cd -
|
||||||
# Unfortunately this edits Info.plist directly.
|
# Unfortunately this edits Info.plist directly.
|
||||||
#xcrun agvtool new-marketing-version 0.4.5
|
#xcrun agvtool new-marketing-version 0.4.5
|
||||||
```
|
```
|
||||||
@@ -1419,8 +1419,8 @@ The recommended way to build for Android is using the automated build script:
|
|||||||
##### 1. Bump the version in package.json, then update these versions & run:
|
##### 1. Bump the version in package.json, then update these versions & run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
perl -p -i -e 's/versionCode .*/versionCode 67/g' android/app/build.gradle
|
perl -p -i -e 's/versionCode .*/versionCode 69/g' android/app/build.gradle
|
||||||
perl -p -i -e 's/versionName .*/versionName "1.3.12"/g' android/app/build.gradle
|
perl -p -i -e 's/versionName .*/versionName "1.4.3"/g' android/app/build.gradle
|
||||||
```
|
```
|
||||||
|
|
||||||
##### 2. Build
|
##### 2. Build
|
||||||
|
|||||||
20
CHANGELOG.md
20
CHANGELOG.md
@@ -6,6 +6,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
|
||||||
|
## [1.4.3] - 2026.06.19
|
||||||
|
### Removed
|
||||||
|
- Automatic "Check your starred projects" daily notification
|
||||||
|
### Changed
|
||||||
|
- Positioning for 'Thank' button and entity-type toggle link
|
||||||
|
|
||||||
|
|
||||||
|
## [1.4.2] - 2026.05.24
|
||||||
|
### Changed
|
||||||
|
- Support 16 KB page sizes
|
||||||
|
|
||||||
|
|
||||||
|
## [1.3.13] - 2026.04.05
|
||||||
|
### Added
|
||||||
|
- Ability to select project that the current one fulfills
|
||||||
|
- Separate Terms & Conditions page (required for SMS campaigns)
|
||||||
|
### Fixed
|
||||||
|
- Edits to a 'give' would delete the image
|
||||||
|
|
||||||
|
|
||||||
## [1.3.12] - 2026.03.21
|
## [1.3.12] - 2026.03.21
|
||||||
### Added
|
### Added
|
||||||
- Device wake-up for notifications
|
- Device wake-up for notifications
|
||||||
|
|||||||
23
README.md
23
README.md
@@ -15,10 +15,31 @@ Quick start:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Web
|
||||||
|
|
||||||
|
```bash
|
||||||
npm run build:web:dev
|
npm run build:web:dev
|
||||||
```
|
```
|
||||||
|
|
||||||
To be able to take action on the platform: go to [the test page](http://localhost:8080/test) and click "Become User 0".
|
Then go to [the test page](http://localhost:8080/test) and click "Become User 0" to take action on the platform.
|
||||||
|
|
||||||
|
### Android
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build:android:test:run
|
||||||
|
```
|
||||||
|
|
||||||
|
Assumes ADB is installed; see [Android Build](BUILDING.md#android-build) for SDK, emulator, and `PATH` setup.
|
||||||
|
|
||||||
|
### iOS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build:ios:studio
|
||||||
|
```
|
||||||
|
|
||||||
|
Assumes Xcode and Xcode Command Line Tools are installed.
|
||||||
|
|
||||||
See [BUILDING.md](BUILDING.md) for comprehensive build instructions for all platforms (Web, Electron, iOS, Android, Docker).
|
See [BUILDING.md](BUILDING.md) for comprehensive build instructions for all platforms (Web, Electron, iOS, Android, Docker).
|
||||||
|
|
||||||
|
|||||||
@@ -29,16 +29,16 @@ android {
|
|||||||
compileSdk rootProject.ext.compileSdkVersion
|
compileSdk rootProject.ext.compileSdkVersion
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_17
|
sourceCompatibility JavaVersion.VERSION_21
|
||||||
targetCompatibility JavaVersion.VERSION_17
|
targetCompatibility JavaVersion.VERSION_21
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "app.timesafari.app"
|
applicationId "app.timesafari.app"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 67
|
versionCode 69
|
||||||
versionName "1.3.12"
|
versionName "1.4.3"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
@@ -72,13 +72,14 @@ android {
|
|||||||
}
|
}
|
||||||
packagingOptions {
|
packagingOptions {
|
||||||
jniLibs {
|
jniLibs {
|
||||||
|
// Required for 16 KB page-size support: keep native libs uncompressed and
|
||||||
|
// page-aligned inside the APK (default on AGP 8.x with minSdk 23+, set
|
||||||
|
// explicitly so it does not regress).
|
||||||
|
useLegacyPackaging = false
|
||||||
pickFirsts += ['**/lib/x86_64/libbarhopper_v3.so', '**/lib/x86_64/libimage_processing_util_jni.so', '**/lib/x86_64/libsqlcipher.so']
|
pickFirsts += ['**/lib/x86_64/libbarhopper_v3.so', '**/lib/x86_64/libimage_processing_util_jni.so', '**/lib/x86_64/libsqlcipher.so']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure for 16 KB page size compatibility
|
|
||||||
|
|
||||||
|
|
||||||
// Enable bundle builds (without which it doesn't work right for bundleDebug vs bundleRelease)
|
// Enable bundle builds (without which it doesn't work right for bundleDebug vs bundleRelease)
|
||||||
bundle {
|
bundle {
|
||||||
language {
|
language {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_17
|
sourceCompatibility JavaVersion.VERSION_21
|
||||||
targetCompatibility JavaVersion.VERSION_17
|
targetCompatibility JavaVersion.VERSION_21
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"appId": "app.timesafari",
|
"appId": "app.timesafari",
|
||||||
"appName": "TimeSafari",
|
"appName": "Giftopia",
|
||||||
"webDir": "dist",
|
"webDir": "dist",
|
||||||
"server": {
|
"server": {
|
||||||
"cleartext": true
|
"cleartext": true
|
||||||
@@ -34,12 +34,12 @@
|
|||||||
"iosIsEncryption": false,
|
"iosIsEncryption": false,
|
||||||
"iosBiometric": {
|
"iosBiometric": {
|
||||||
"biometricAuth": false,
|
"biometricAuth": false,
|
||||||
"biometricTitle": "Biometric login for TimeSafari"
|
"biometricTitle": "Biometric login for Giftopia"
|
||||||
},
|
},
|
||||||
"androidIsEncryption": false,
|
"androidIsEncryption": false,
|
||||||
"androidBiometric": {
|
"androidBiometric": {
|
||||||
"biometricAuth": false,
|
"biometricAuth": false,
|
||||||
"biometricTitle": "Biometric login for TimeSafari"
|
"biometricTitle": "Biometric login for Giftopia"
|
||||||
},
|
},
|
||||||
"electronIsEncryption": false
|
"electronIsEncryption": false
|
||||||
},
|
},
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
},
|
},
|
||||||
"buildOptions": {
|
"buildOptions": {
|
||||||
"appId": "app.timesafari",
|
"appId": "app.timesafari",
|
||||||
"productName": "TimeSafari",
|
"productName": "Giftopia",
|
||||||
"directories": {
|
"directories": {
|
||||||
"output": "dist-electron-packages"
|
"output": "dist-electron-packages"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import org.timesafari.dailynotification.FetchContext;
|
|||||||
import org.timesafari.dailynotification.NativeNotificationContentFetcher;
|
import org.timesafari.dailynotification.NativeNotificationContentFetcher;
|
||||||
import org.timesafari.dailynotification.NotificationContent;
|
import org.timesafari.dailynotification.NotificationContent;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
@@ -47,22 +46,11 @@ public class TimeSafariNativeFetcher implements NativeNotificationContentFetcher
|
|||||||
// This should query the TimeSafari API for notification content
|
// This should query the TimeSafari API for notification content
|
||||||
// using the configured apiBaseUrl, activeDid, and jwtToken
|
// using the configured apiBaseUrl, activeDid, and jwtToken
|
||||||
|
|
||||||
// For now, return a placeholder notification
|
// Not implemented yet: return no content rather than fabricating a
|
||||||
long scheduledTime = fetchContext.scheduledTime != null
|
// placeholder notification (previously hardcoded "Check your starred
|
||||||
? fetchContext.scheduledTime
|
// projects for updates!", which showed on every app startup).
|
||||||
: System.currentTimeMillis() + 60000; // 1 minute from now
|
Log.d(TAG, "Content fetching not yet implemented; returning no notifications");
|
||||||
|
return Collections.<NotificationContent>emptyList();
|
||||||
NotificationContent content = new NotificationContent(
|
|
||||||
"TimeSafari Update",
|
|
||||||
"Check your starred projects for updates!",
|
|
||||||
scheduledTime
|
|
||||||
);
|
|
||||||
|
|
||||||
List<NotificationContent> results = new ArrayList<>();
|
|
||||||
results.add(content);
|
|
||||||
|
|
||||||
Log.d(TAG, "Returning " + results.size() + " notification(s)");
|
|
||||||
return results;
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e(TAG, "Fetch failed", e);
|
Log.e(TAG, "Fetch failed", e);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
ext {
|
ext {
|
||||||
androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.6.1'
|
androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.0'
|
||||||
cordovaAndroidVersion = project.hasProperty('cordovaAndroidVersion') ? rootProject.ext.cordovaAndroidVersion : '10.1.1'
|
cordovaAndroidVersion = project.hasProperty('cordovaAndroidVersion') ? rootProject.ext.cordovaAndroidVersion : '10.1.1'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ buildscript {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:8.2.1'
|
classpath 'com.android.tools.build:gradle:8.7.2'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,10 +17,10 @@ apply plugin: 'com.android.library'
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace "capacitor.cordova.android.plugins"
|
namespace "capacitor.cordova.android.plugins"
|
||||||
compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 34
|
compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 35
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 22
|
minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 23
|
||||||
targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 34
|
targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 35
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "1.0"
|
versionName "1.0"
|
||||||
}
|
}
|
||||||
@@ -28,8 +28,8 @@ android {
|
|||||||
abortOnError false
|
abortOnError false
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_17
|
sourceCompatibility JavaVersion.VERSION_21
|
||||||
targetCompatibility JavaVersion.VERSION_17
|
targetCompatibility JavaVersion.VERSION_21
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
||||||
ext {
|
ext {
|
||||||
cdvMinSdkVersion = project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 22
|
cdvMinSdkVersion = project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 23
|
||||||
// Plugin gradle extensions can append to this to have code run at the end.
|
// Plugin gradle extensions can append to this to have code run at the end.
|
||||||
cdvPluginPostBuildExtras = []
|
cdvPluginPostBuildExtras = []
|
||||||
cordovaConfig = [:]
|
cordovaConfig = [:]
|
||||||
|
|||||||
@@ -13,4 +13,11 @@ ext {
|
|||||||
androidxJunitVersion = '1.1.5'
|
androidxJunitVersion = '1.1.5'
|
||||||
androidxEspressoCoreVersion = '3.5.1'
|
androidxEspressoCoreVersion = '3.5.1'
|
||||||
cordovaAndroidVersion = '10.1.1'
|
cordovaAndroidVersion = '10.1.1'
|
||||||
|
|
||||||
|
// Pin CameraX to 1.4.2: first stable line shipping a 16 KB page-size-aligned
|
||||||
|
// libimage_processing_util_jni.so. The barcode-scanning plugin still defaults to 1.1.0.
|
||||||
|
androidxCameraCamera2Version = '1.4.2'
|
||||||
|
androidxCameraCoreVersion = '1.4.2'
|
||||||
|
androidxCameraLifecycleVersion = '1.4.2'
|
||||||
|
androidxCameraViewVersion = '1.4.2'
|
||||||
}
|
}
|
||||||
92
doc/share-target-ios-audit.md
Normal file
92
doc/share-target-ios-audit.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# iOS Share Target Audit
|
||||||
|
|
||||||
|
**Date:** 2026-06-30
|
||||||
|
**Platform:** iOS only (Android unchanged)
|
||||||
|
**Status:** Phase 1D complete — explicit share completion API added (not yet called)
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This document tracks the native iOS share-target lifecycle and the
|
||||||
|
Capacitor API surface exposed to the web/TypeScript layer. It is updated as
|
||||||
|
each phase lands.
|
||||||
|
|
||||||
|
## Lifecycle Overview
|
||||||
|
|
||||||
|
1. The Share Extension (`ShareViewController.swift`) receives an image, writes
|
||||||
|
it to the App Group container, records metadata in App Group
|
||||||
|
`UserDefaults`, assigns a `shareId` (UUID), sets the `sharedPhotoReady`
|
||||||
|
flag, and opens the main app via `timesafari://`.
|
||||||
|
2. The main app reads the shared image through the `SharedImage` Capacitor
|
||||||
|
plugin.
|
||||||
|
3. The application layer processes the image, then (in a future phase) signals
|
||||||
|
explicit completion via `markProcessed(shareId)`.
|
||||||
|
|
||||||
|
## App Group Storage Keys
|
||||||
|
|
||||||
|
Stored in `UserDefaults(suiteName: "group.app.trentlarson.timesafari.share")`:
|
||||||
|
|
||||||
|
| Key | Set by | Meaning |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `sharedPhotoFileName` | Share Extension | Original display filename |
|
||||||
|
| `sharedPhotoFilePath` | Share Extension | On-disk filename in App Group container |
|
||||||
|
| `sharedPhotoShareId` | Share Extension | Current pending share identifier (UUID) |
|
||||||
|
| `sharedPhotoReady` | Share Extension | Flag indicating a share is ready |
|
||||||
|
| `sharedPhotoProcessedShareId` | Main app (`markProcessed`) | Last shareId explicitly marked processed (Phase 1D) |
|
||||||
|
|
||||||
|
## Capacitor API (`SharedImage` plugin)
|
||||||
|
|
||||||
|
The plugin is registered as `SharedImage` and is implemented in:
|
||||||
|
|
||||||
|
- Native iOS: `ios/App/App/SharedImagePlugin.swift` (delegates to
|
||||||
|
`ios/App/App/SharedImageUtility.swift`)
|
||||||
|
- Web stub: `src/plugins/SharedImagePlugin.web.ts`
|
||||||
|
- TypeScript definitions: `src/plugins/definitions.ts`
|
||||||
|
|
||||||
|
### `getSharedImage(): Promise<SharedImageResult | null>`
|
||||||
|
|
||||||
|
Returns the shared image as `{ base64, fileName, shareId }`, or `null` if none
|
||||||
|
exists. `shareId` is the native `sharedPhotoShareId` (UUID assigned by the
|
||||||
|
Share Extension); pass it to `markProcessed()` to signal explicit completion.
|
||||||
|
Read-only: native metadata and the file are left intact after retrieval
|
||||||
|
(Phase 1C).
|
||||||
|
|
||||||
|
> Note: On iOS the "no shared image" case resolves an object with `base64`,
|
||||||
|
> `fileName`, and `shareId` set to `null`; on web `getSharedImage()` resolves
|
||||||
|
> `null` directly.
|
||||||
|
|
||||||
|
### `hasSharedImage(): Promise<{ hasImage: boolean }>`
|
||||||
|
|
||||||
|
Returns whether a shared image file currently exists, without reading it.
|
||||||
|
|
||||||
|
### `markProcessed(options: { shareId: string }): Promise<{ success: boolean }>` — Phase 1D
|
||||||
|
|
||||||
|
Explicit completion signal from the application layer for a given share.
|
||||||
|
|
||||||
|
- **Input:** `shareId` — the identifier reported by `getSharedImage()`.
|
||||||
|
Must be a non-empty string; otherwise the call rejects.
|
||||||
|
- **Behavior (iOS):** Records `shareId` in App Group `UserDefaults` under
|
||||||
|
`sharedPhotoProcessedShareId` and resolves with `{ success: true }`.
|
||||||
|
- **Does NOT delete files:** The stored image file and share metadata are
|
||||||
|
intentionally left intact. File cleanup is deferred to a later phase.
|
||||||
|
- **Web:** No-op; resolves with `{ success: false }`.
|
||||||
|
- **Logging:** Emits `[ShareTarget] shareId=<id> marked processed` on success.
|
||||||
|
|
||||||
|
> Note (Phase 1D): `markProcessed` is implemented and exposed to TypeScript
|
||||||
|
> but is **not yet called** from application code. Wiring it into the
|
||||||
|
> processing flow is a later phase.
|
||||||
|
|
||||||
|
## Native Logging Reference
|
||||||
|
|
||||||
|
Relevant `[ShareTarget]` log lines emitted on iOS:
|
||||||
|
|
||||||
|
- `[ShareTarget] shareId=<id> retrieved`
|
||||||
|
- `[ShareTarget] shareId=<id> left intact after retrieval`
|
||||||
|
- `[ShareTarget] shareId=<id> marked processed` (Phase 1D)
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- Android: no changes. The Android plugin
|
||||||
|
(`android/app/src/main/java/app/timesafari/sharedimage/SharedImagePlugin.java`)
|
||||||
|
does not implement `markProcessed`.
|
||||||
|
- File deletion / cleanup of processed shares.
|
||||||
|
- Calling `markProcessed` from application code.
|
||||||
3
ios/.gitignore
vendored
3
ios/.gitignore
vendored
@@ -17,6 +17,7 @@ App/App/config.xml
|
|||||||
App/App.xcodeproj/xcuserdata/*.xcuserdatad/
|
App/App.xcodeproj/xcuserdata/*.xcuserdatad/
|
||||||
App/App.xcodeproj/*.xcuserstate
|
App/App.xcodeproj/*.xcuserstate
|
||||||
|
|
||||||
# Generated Icons from capacitor-assets (also Contents.json which is confusing; see BUILDING.md)
|
# Generated by capacitor-assets at build time (not in repo). Fresh clones lack these
|
||||||
|
# folders; scripts/common.sh ensure_ios_capacitor_asset_directories creates them before generate.
|
||||||
App/App/Assets.xcassets/AppIcon.appiconset
|
App/App/Assets.xcassets/AppIcon.appiconset
|
||||||
App/App/Assets.xcassets/Splash.imageset
|
App/App/Assets.xcassets/Splash.imageset
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
C86585DF2ED456DE00824752 /* TimeSafariShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = C86585D52ED456DE00824752 /* TimeSafariShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
C86585DF2ED456DE00824752 /* TimeSafariShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = C86585D52ED456DE00824752 /* TimeSafariShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
C8C56E142EE0474B00737D0E /* SharedImageUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8C56E132EE0474B00737D0E /* SharedImageUtility.swift */; };
|
C8C56E142EE0474B00737D0E /* SharedImageUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8C56E132EE0474B00737D0E /* SharedImageUtility.swift */; };
|
||||||
C8C56E162EE064CB00737D0E /* SharedImagePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8C56E152EE064CA00737D0E /* SharedImagePlugin.swift */; };
|
C8C56E162EE064CB00737D0E /* SharedImagePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8C56E152EE064CA00737D0E /* SharedImagePlugin.swift */; };
|
||||||
|
C8C56E182EE0700A00737D0E /* AppBridgeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8C56E172EE0700A00737D0E /* AppBridgeViewController.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -59,6 +60,7 @@
|
|||||||
C86585E52ED4577F00824752 /* App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = App.entitlements; sourceTree = "<group>"; };
|
C86585E52ED4577F00824752 /* App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = App.entitlements; sourceTree = "<group>"; };
|
||||||
C8C56E132EE0474B00737D0E /* SharedImageUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedImageUtility.swift; sourceTree = "<group>"; };
|
C8C56E132EE0474B00737D0E /* SharedImageUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedImageUtility.swift; sourceTree = "<group>"; };
|
||||||
C8C56E152EE064CA00737D0E /* SharedImagePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedImagePlugin.swift; sourceTree = "<group>"; };
|
C8C56E152EE064CA00737D0E /* SharedImagePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedImagePlugin.swift; sourceTree = "<group>"; };
|
||||||
|
C8C56E172EE0700A00737D0E /* AppBridgeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppBridgeViewController.swift; sourceTree = "<group>"; };
|
||||||
E2E9297D5D02C549106C77F9 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
|
E2E9297D5D02C549106C77F9 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
EAEC6436E595F7CD3A1C9E96 /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
|
EAEC6436E595F7CD3A1C9E96 /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
@@ -74,18 +76,7 @@
|
|||||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
C86585D62ED456DE00824752 /* TimeSafariShareExtension */ = {
|
C86585D62ED456DE00824752 /* TimeSafariShareExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (C86585E32ED456DE00824752 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = TimeSafariShareExtension; sourceTree = "<group>"; };
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
|
||||||
exceptions = (
|
|
||||||
C86585E32ED456DE00824752 /* PBXFileSystemSynchronizedBuildFileExceptionSet */,
|
|
||||||
);
|
|
||||||
explicitFileTypes = {
|
|
||||||
};
|
|
||||||
explicitFolders = (
|
|
||||||
);
|
|
||||||
path = TimeSafariShareExtension;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -138,6 +129,7 @@
|
|||||||
504EC3061FED79650016851F /* App */ = {
|
504EC3061FED79650016851F /* App */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
C8C56E172EE0700A00737D0E /* AppBridgeViewController.swift */,
|
||||||
C8C56E152EE064CA00737D0E /* SharedImagePlugin.swift */,
|
C8C56E152EE064CA00737D0E /* SharedImagePlugin.swift */,
|
||||||
C8C56E132EE0474B00737D0E /* SharedImageUtility.swift */,
|
C8C56E132EE0474B00737D0E /* SharedImageUtility.swift */,
|
||||||
C86585E52ED4577F00824752 /* App.entitlements */,
|
C86585E52ED4577F00824752 /* App.entitlements */,
|
||||||
@@ -357,6 +349,7 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
C8C56E162EE064CB00737D0E /* SharedImagePlugin.swift in Sources */,
|
C8C56E162EE064CB00737D0E /* SharedImagePlugin.swift in Sources */,
|
||||||
|
C8C56E182EE0700A00737D0E /* AppBridgeViewController.swift in Sources */,
|
||||||
504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
|
504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
|
||||||
C8C56E142EE0474B00737D0E /* SharedImageUtility.swift in Sources */,
|
C8C56E142EE0474B00737D0E /* SharedImageUtility.swift in Sources */,
|
||||||
);
|
);
|
||||||
@@ -452,7 +445,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -508,7 +501,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
@@ -524,18 +517,18 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
|
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 67;
|
CURRENT_PROJECT_VERSION = 69;
|
||||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||||
ENABLE_APP_SANDBOX = NO;
|
ENABLE_APP_SANDBOX = NO;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = Giftopia;
|
INFOPLIST_KEY_CFBundleDisplayName = Giftopia;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.5;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.3.12;
|
MARKETING_VERSION = 1.4.3;
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@@ -553,18 +546,18 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
|
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 67;
|
CURRENT_PROJECT_VERSION = 69;
|
||||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||||
ENABLE_APP_SANDBOX = NO;
|
ENABLE_APP_SANDBOX = NO;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = Giftopia;
|
INFOPLIST_KEY_CFBundleDisplayName = Giftopia;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.5;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.3.12;
|
MARKETING_VERSION = 1.4.3;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||||
@@ -582,7 +575,7 @@
|
|||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = TimeSafariShareExtension/TimeSafariShareExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = TimeSafariShareExtension/TimeSafariShareExtension.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 67;
|
CURRENT_PROJECT_VERSION = 69;
|
||||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -596,7 +589,7 @@
|
|||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
MARKETING_VERSION = 1.3.12;
|
MARKETING_VERSION = 1.4.3;
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari.TimeSafariShareExtension;
|
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari.TimeSafariShareExtension;
|
||||||
@@ -620,7 +613,7 @@
|
|||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = TimeSafariShareExtension/TimeSafariShareExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = TimeSafariShareExtension/TimeSafariShareExtension.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 67;
|
CURRENT_PROJECT_VERSION = 69;
|
||||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -634,7 +627,7 @@
|
|||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
MARKETING_VERSION = 1.3.12;
|
MARKETING_VERSION = 1.4.3;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari.TimeSafariShareExtension;
|
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari.TimeSafariShareExtension;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
|||||||
24
ios/App/App/AppBridgeViewController.swift
Normal file
24
ios/App/App/AppBridgeViewController.swift
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
//
|
||||||
|
// AppBridgeViewController.swift
|
||||||
|
// App
|
||||||
|
//
|
||||||
|
// Capacitor bridge view controller subclass.
|
||||||
|
//
|
||||||
|
// Registers the app-local SharedImagePlugin once the Capacitor bridge
|
||||||
|
// has been created. This provides deterministic plugin registration
|
||||||
|
// before the web layer starts executing.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Capacitor
|
||||||
|
|
||||||
|
class AppBridgeViewController: CAPBridgeViewController {
|
||||||
|
|
||||||
|
override func capacitorDidLoad() {
|
||||||
|
super.capacitorDidLoad()
|
||||||
|
|
||||||
|
let pluginInstance = SharedImagePlugin()
|
||||||
|
bridge?.registerPluginInstance(pluginInstance)
|
||||||
|
print("[AppBridgeViewController] ✅ Registered SharedImagePlugin (exposed as 'SharedImage' via @objc annotation)")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,50 +15,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||||||
// Initialize SQLite
|
// Initialize SQLite
|
||||||
//let sqlite = SQLite()
|
//let sqlite = SQLite()
|
||||||
//sqlite.initialize()
|
//sqlite.initialize()
|
||||||
|
|
||||||
// Register SharedImage plugin manually after bridge is ready
|
// SharedImagePlugin is registered from AppBridgeViewController.capacitorDidLoad()
|
||||||
// Try multiple times with increasing delays to ensure bridge is initialized
|
// (Phase 2B-2). The previous AppDelegate retry-based registration was removed.
|
||||||
var attempts = 0
|
|
||||||
let maxAttempts = 5
|
|
||||||
|
|
||||||
func tryRegister() {
|
|
||||||
attempts += 1
|
|
||||||
if registerSharedImagePlugin() {
|
|
||||||
print("[AppDelegate] ✅ Plugin registration successful on attempt \(attempts)")
|
|
||||||
} else if attempts < maxAttempts {
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + Double(attempts) * 0.5) {
|
|
||||||
tryRegister()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
print("[AppDelegate] ⚠️ Failed to register plugin after \(maxAttempts) attempts")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start registration attempts
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
||||||
tryRegister()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Override point for customization after application launch.
|
// Override point for customization after application launch.
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
|
||||||
private func registerSharedImagePlugin() -> Bool {
|
|
||||||
guard let window = self.window,
|
|
||||||
let bridgeVC = window.rootViewController as? CAPBridgeViewController,
|
|
||||||
let bridge = bridgeVC.bridge else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create plugin instance
|
|
||||||
// The @objc(SharedImage) annotation makes it available as "SharedImage" to Objective-C
|
|
||||||
// which matches the JavaScript registration name
|
|
||||||
let pluginInstance = SharedImagePlugin()
|
|
||||||
bridge.registerPluginInstance(pluginInstance)
|
|
||||||
print("[AppDelegate] ✅ Registered SharedImagePlugin (exposed as 'SharedImage' via @objc annotation)")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func applicationWillResignActive(_ application: UIApplication) {
|
func applicationWillResignActive(_ application: UIApplication) {
|
||||||
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
|
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
|
||||||
@@ -116,7 +79,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||||||
*/
|
*/
|
||||||
private func checkForSharedImageOnActivation() {
|
private func checkForSharedImageOnActivation() {
|
||||||
// Check if shared photo is ready
|
// Check if shared photo is ready
|
||||||
if SharedImageUtility.isSharedPhotoReady() {
|
let isReady = SharedImageUtility.isSharedPhotoReady()
|
||||||
|
if isReady {
|
||||||
// Clear the flag
|
// Clear the flag
|
||||||
SharedImageUtility.clearSharedPhotoReadyFlag()
|
SharedImageUtility.clearSharedPhotoReadyFlag()
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<!--Bridge View Controller-->
|
<!--Bridge View Controller-->
|
||||||
<scene sceneID="tne-QT-ifu">
|
<scene sceneID="tne-QT-ifu">
|
||||||
<objects>
|
<objects>
|
||||||
<viewController id="BYZ-38-t0r" customClass="CAPBridgeViewController" customModule="Capacitor" sceneMemberID="viewController"/>
|
<viewController id="BYZ-38-t0r" customClass="AppBridgeViewController" customModule="App" customModuleProvider="target" sceneMemberID="viewController"/>
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||||
</objects>
|
</objects>
|
||||||
</scene>
|
</scene>
|
||||||
|
|||||||
@@ -2,6 +2,13 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||||
|
<array>
|
||||||
|
<string>org.timesafari.dailynotification.fetch</string>
|
||||||
|
<string>org.timesafari.dailynotification.notify</string>
|
||||||
|
<string>org.timesafari.dailynotification.content-fetch</string>
|
||||||
|
<string>org.timesafari.dailynotification.notification-delivery</string>
|
||||||
|
</array>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>en</string>
|
<string>en</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
@@ -18,6 +25,17 @@
|
|||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>$(MARKETING_VERSION)</string>
|
<string>$(MARKETING_VERSION)</string>
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>app.timesafari</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>timesafari</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
@@ -26,6 +44,13 @@
|
|||||||
<string>Time Safari allows you to take photos, and also scan QR codes from contacts.</string>
|
<string>Time Safari allows you to take photos, and also scan QR codes from contacts.</string>
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
<string>Time Safari allows you to upload photos.</string>
|
<string>Time Safari allows you to upload photos.</string>
|
||||||
|
<key>NSUserNotificationAlertStyle</key>
|
||||||
|
<string>alert</string>
|
||||||
|
<key>UIBackgroundModes</key>
|
||||||
|
<array>
|
||||||
|
<string>fetch</string>
|
||||||
|
<string>processing</string>
|
||||||
|
</array>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
@@ -47,30 +72,5 @@
|
|||||||
</array>
|
</array>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>CFBundleURLTypes</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleURLName</key>
|
|
||||||
<string>app.timesafari</string>
|
|
||||||
<key>CFBundleURLSchemes</key>
|
|
||||||
<array>
|
|
||||||
<string>timesafari</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
<key>UIBackgroundModes</key>
|
|
||||||
<array>
|
|
||||||
<string>fetch</string>
|
|
||||||
<string>processing</string>
|
|
||||||
</array>
|
|
||||||
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
|
||||||
<array>
|
|
||||||
<string>org.timesafari.dailynotification.fetch</string>
|
|
||||||
<string>org.timesafari.dailynotification.notify</string>
|
|
||||||
<string>org.timesafari.dailynotification.content-fetch</string>
|
|
||||||
<string>org.timesafari.dailynotification.notification-delivery</string>
|
|
||||||
</array>
|
|
||||||
<key>NSUserNotificationAlertStyle</key>
|
|
||||||
<string>alert</string>
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ public class SharedImagePlugin: CAPPlugin, CAPBridgedPlugin {
|
|||||||
public var pluginMethods: [CAPPluginMethod] {
|
public var pluginMethods: [CAPPluginMethod] {
|
||||||
return [
|
return [
|
||||||
CAPPluginMethod(#selector(getSharedImage(_:)), returnType: .promise),
|
CAPPluginMethod(#selector(getSharedImage(_:)), returnType: .promise),
|
||||||
CAPPluginMethod(#selector(hasSharedImage(_:)), returnType: .promise)
|
CAPPluginMethod(#selector(hasSharedImage(_:)), returnType: .promise),
|
||||||
|
CAPPluginMethod(#selector(markProcessed(_:)), returnType: .promise),
|
||||||
|
CAPPluginMethod(#selector(clearProcessedShare(_:)), returnType: .promise)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,23 +34,21 @@ public class SharedImagePlugin: CAPPlugin, CAPBridgedPlugin {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get shared image data from App Group UserDefaults
|
* Get shared image data from App Group UserDefaults
|
||||||
* Returns base64 string and fileName, or null if no image exists
|
* Returns base64 string, fileName, and shareId, or null if no image exists
|
||||||
* Clears the data after reading to prevent re-reading
|
* Read-only: native metadata and file are left intact after retrieval (Phase 1C)
|
||||||
*/
|
*/
|
||||||
@objc public func getSharedImage(_ call: CAPPluginCall) {
|
@objc public func getSharedImage(_ call: CAPPluginCall) {
|
||||||
guard let sharedData = SharedImageUtility.getSharedImageData() else {
|
guard let sharedData = SharedImageUtility.getSharedImageData() else {
|
||||||
// No shared image exists - return null (not an error)
|
// No shared image exists - return null (not an error)
|
||||||
call.resolve([
|
call.resolve()
|
||||||
"base64": NSNull(),
|
|
||||||
"fileName": NSNull()
|
|
||||||
])
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the shared image data
|
// Return the shared image data
|
||||||
call.resolve([
|
call.resolve([
|
||||||
"base64": sharedData["base64"] ?? "",
|
"base64": sharedData["base64"] ?? "",
|
||||||
"fileName": sharedData["fileName"] ?? ""
|
"fileName": sharedData["fileName"] ?? "",
|
||||||
|
"shareId": sharedData["shareId"] ?? ""
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,5 +62,34 @@ public class SharedImagePlugin: CAPPlugin, CAPBridgedPlugin {
|
|||||||
"hasImage": hasImage
|
"hasImage": hasImage
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark a share as processed by the application layer (Phase 1D)
|
||||||
|
* Records an explicit completion signal for the given shareId.
|
||||||
|
* Does NOT delete the stored image file - file cleanup is deferred to a later phase.
|
||||||
|
*/
|
||||||
|
@objc public func markProcessed(_ call: CAPPluginCall) {
|
||||||
|
guard let shareId = call.getString("shareId"), !shareId.isEmpty else {
|
||||||
|
call.reject("markProcessed requires a non-empty 'shareId'")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let success = SharedImageUtility.markProcessed(shareId: shareId)
|
||||||
|
call.resolve([
|
||||||
|
"success": success
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Permanently remove the currently processed share from App Group storage (Phase 1F)
|
||||||
|
* Deletes the image file and all share metadata. Idempotent.
|
||||||
|
* Intended to be called only after successful processing and markProcessed().
|
||||||
|
*/
|
||||||
|
@objc public func clearProcessedShare(_ call: CAPPluginCall) {
|
||||||
|
let success = SharedImageUtility.clearProcessedShare()
|
||||||
|
call.resolve([
|
||||||
|
"success": success
|
||||||
|
])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,22 +13,44 @@ public class SharedImageUtility {
|
|||||||
private static let appGroupIdentifier = "group.app.timesafari.share"
|
private static let appGroupIdentifier = "group.app.timesafari.share"
|
||||||
private static let sharedPhotoFileNameKey = "sharedPhotoFileName"
|
private static let sharedPhotoFileNameKey = "sharedPhotoFileName"
|
||||||
private static let sharedPhotoFilePathKey = "sharedPhotoFilePath"
|
private static let sharedPhotoFilePathKey = "sharedPhotoFilePath"
|
||||||
|
private static let sharedPhotoShareIdKey = "sharedPhotoShareId"
|
||||||
private static let sharedPhotoReadyKey = "sharedPhotoReady"
|
private static let sharedPhotoReadyKey = "sharedPhotoReady"
|
||||||
|
private static let sharedPhotoProcessedShareIdKey = "sharedPhotoProcessedShareId"
|
||||||
|
|
||||||
/// Get the App Group container URL for accessing shared files
|
/// Get the App Group container URL for accessing shared files
|
||||||
private static var appGroupContainerURL: URL? {
|
private static var appGroupContainerURL: URL? {
|
||||||
return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)
|
return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func logShareDiagnostic(method: String, userDefaults: UserDefaults?) {
|
||||||
|
let shareId = userDefaults?.string(forKey: sharedPhotoShareIdKey)
|
||||||
|
let filePath = userDefaults?.string(forKey: sharedPhotoFilePathKey)
|
||||||
|
let metadataExists = filePath != nil
|
||||||
|
let fileExists: Bool
|
||||||
|
if let filePath = filePath, let containerURL = appGroupContainerURL {
|
||||||
|
let fileURL = containerURL.appendingPathComponent(filePath)
|
||||||
|
fileExists = FileManager.default.fileExists(atPath: fileURL.path)
|
||||||
|
} else {
|
||||||
|
fileExists = false
|
||||||
|
}
|
||||||
|
|
||||||
|
let shareIdLog = shareId ?? "nil"
|
||||||
|
let filePathLog = filePath ?? "nil"
|
||||||
|
print("[ShareTarget] \(method) shareId=\(shareIdLog) sharedPhotoFilePath=\(filePathLog) metadataExists=\(metadataExists) fileExists=\(fileExists)")
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get shared image data from App Group container file
|
* Get shared image data from App Group container file
|
||||||
* All images are stored as files for consistency and to avoid UserDefaults size limits
|
* All images are stored as files for consistency and to avoid UserDefaults size limits
|
||||||
* Clears the data after reading to prevent re-reading
|
* Read-only: metadata and file are left intact after retrieval (Phase 1C)
|
||||||
*
|
*
|
||||||
* @returns Dictionary with "base64" and "fileName" keys, or nil if no shared image
|
* @returns Dictionary with "base64", "fileName", and "shareId" keys, or nil if no shared image
|
||||||
*/
|
*/
|
||||||
static func getSharedImageData() -> [String: String]? {
|
static func getSharedImageData() -> [String: String]? {
|
||||||
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
|
let userDefaults = UserDefaults(suiteName: appGroupIdentifier)
|
||||||
|
logShareDiagnostic(method: "getSharedImageData", userDefaults: userDefaults)
|
||||||
|
|
||||||
|
guard let userDefaults = userDefaults else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,26 +61,110 @@ public class SharedImageUtility {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let fileName = userDefaults.string(forKey: sharedPhotoFileNameKey) ?? "shared-image.jpg"
|
let fileName = userDefaults.string(forKey: sharedPhotoFileNameKey) ?? "shared-image.jpg"
|
||||||
|
let shareId = userDefaults.string(forKey: sharedPhotoShareIdKey)
|
||||||
|
|
||||||
|
// Phase 1E: skip a share that has already been marked processed.
|
||||||
|
// If the current pending shareId matches the processed marker, treat it
|
||||||
|
// as if no pending share exists. Leave the file and metadata intact -
|
||||||
|
// cleanup is deferred to a later phase.
|
||||||
|
let processedShareId = userDefaults.string(forKey: sharedPhotoProcessedShareIdKey)
|
||||||
|
if let shareId = shareId,
|
||||||
|
let processedShareId = processedShareId,
|
||||||
|
shareId == processedShareId {
|
||||||
|
print("[ShareTarget] shareId=\(shareId) already processed; skipping")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
let fileURL = containerURL.appendingPathComponent(filePath)
|
let fileURL = containerURL.appendingPathComponent(filePath)
|
||||||
|
|
||||||
// Read image data from file
|
// Read image data from file
|
||||||
guard let imageData = try? Data(contentsOf: fileURL) else {
|
guard let imageData = try? Data(contentsOf: fileURL) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let resolvedShareId = shareId ?? "unknown"
|
||||||
|
print("[ShareTarget] shareId=\(resolvedShareId) retrieved")
|
||||||
|
print("[ShareTarget] shareId=\(resolvedShareId) left intact after retrieval")
|
||||||
|
|
||||||
// Convert file data to base64 for JavaScript consumption
|
// Convert file data to base64 for JavaScript consumption
|
||||||
let base64String = imageData.base64EncodedString()
|
let base64String = imageData.base64EncodedString()
|
||||||
|
|
||||||
// Clear the shared data after reading
|
return ["base64": base64String, "fileName": fileName, "shareId": resolvedShareId]
|
||||||
userDefaults.removeObject(forKey: sharedPhotoFilePathKey)
|
}
|
||||||
userDefaults.removeObject(forKey: sharedPhotoFileNameKey)
|
|
||||||
|
/**
|
||||||
// Remove the file
|
* Mark a share as processed by the web/application layer (Phase 1D).
|
||||||
try? FileManager.default.removeItem(at: fileURL)
|
*
|
||||||
|
* Records the processed shareId in the App Group UserDefaults so that
|
||||||
|
* later phases can decide whether retrieval/cleanup should be skipped.
|
||||||
|
* This is an explicit completion signal: it does NOT delete the stored
|
||||||
|
* image file or clear the share metadata. File cleanup is deferred to a
|
||||||
|
* later phase.
|
||||||
|
*
|
||||||
|
* @param shareId The share identifier reported by getSharedImage()
|
||||||
|
* @returns true if the processed marker was recorded, false otherwise
|
||||||
|
*/
|
||||||
|
@discardableResult
|
||||||
|
static func markProcessed(shareId: String) -> Bool {
|
||||||
|
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
|
||||||
|
print("[ShareTarget] shareId=\(shareId) markProcessed failed: UserDefaults unavailable for app group")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
userDefaults.set(shareId, forKey: sharedPhotoProcessedShareIdKey)
|
||||||
userDefaults.synchronize()
|
userDefaults.synchronize()
|
||||||
|
|
||||||
return ["base64": base64String, "fileName": fileName]
|
print("[ShareTarget] shareId=\(shareId) marked processed")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Permanently remove the currently processed share from App Group storage (Phase 1F).
|
||||||
|
*
|
||||||
|
* Deletes the shared image file (if present) and removes all share metadata
|
||||||
|
* keys. Idempotent: missing files or keys are treated as success. Intended
|
||||||
|
* to be called only after the share has been successfully handed off to the
|
||||||
|
* app and marked processed.
|
||||||
|
*
|
||||||
|
* @returns true if cleanup completed without error, false if file deletion failed
|
||||||
|
*/
|
||||||
|
@discardableResult
|
||||||
|
static func clearProcessedShare() -> Bool {
|
||||||
|
let userDefaults = UserDefaults(suiteName: appGroupIdentifier)
|
||||||
|
let shareId = userDefaults?.string(forKey: sharedPhotoShareIdKey)
|
||||||
|
?? userDefaults?.string(forKey: sharedPhotoProcessedShareIdKey)
|
||||||
|
?? "unknown"
|
||||||
|
|
||||||
|
print("[ShareTarget] shareId=\(shareId) cleanup started")
|
||||||
|
|
||||||
|
var success = true
|
||||||
|
|
||||||
|
// Delete the shared image file if it exists
|
||||||
|
if let filePath = userDefaults?.string(forKey: sharedPhotoFilePathKey),
|
||||||
|
let containerURL = appGroupContainerURL {
|
||||||
|
let fileURL = containerURL.appendingPathComponent(filePath)
|
||||||
|
if FileManager.default.fileExists(atPath: fileURL.path) {
|
||||||
|
do {
|
||||||
|
try FileManager.default.removeItem(at: fileURL)
|
||||||
|
} catch {
|
||||||
|
success = false
|
||||||
|
print("[ShareTarget] shareId=\(shareId) cleanup warning: failed to delete file error=\(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all share metadata keys (removeObject is a no-op when absent)
|
||||||
|
if let userDefaults = userDefaults {
|
||||||
|
userDefaults.removeObject(forKey: sharedPhotoFileNameKey)
|
||||||
|
userDefaults.removeObject(forKey: sharedPhotoFilePathKey)
|
||||||
|
userDefaults.removeObject(forKey: sharedPhotoShareIdKey)
|
||||||
|
userDefaults.removeObject(forKey: sharedPhotoReadyKey)
|
||||||
|
userDefaults.removeObject(forKey: sharedPhotoProcessedShareIdKey)
|
||||||
|
userDefaults.synchronize()
|
||||||
|
}
|
||||||
|
|
||||||
|
print("[ShareTarget] shareId=\(shareId) cleanup completed")
|
||||||
|
return success
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -67,7 +173,10 @@ public class SharedImageUtility {
|
|||||||
* @returns true if shared image file exists, false otherwise
|
* @returns true if shared image file exists, false otherwise
|
||||||
*/
|
*/
|
||||||
static func hasSharedImage() -> Bool {
|
static func hasSharedImage() -> Bool {
|
||||||
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier),
|
let userDefaults = UserDefaults(suiteName: appGroupIdentifier)
|
||||||
|
logShareDiagnostic(method: "hasSharedImage", userDefaults: userDefaults)
|
||||||
|
|
||||||
|
guard let userDefaults = userDefaults,
|
||||||
let filePath = userDefaults.string(forKey: sharedPhotoFilePathKey),
|
let filePath = userDefaults.string(forKey: sharedPhotoFilePathKey),
|
||||||
let containerURL = appGroupContainerURL else {
|
let containerURL = appGroupContainerURL else {
|
||||||
return false
|
return false
|
||||||
@@ -76,7 +185,7 @@ public class SharedImageUtility {
|
|||||||
let fileURL = containerURL.appendingPathComponent(filePath)
|
let fileURL = containerURL.appendingPathComponent(filePath)
|
||||||
return FileManager.default.fileExists(atPath: fileURL.path)
|
return FileManager.default.fileExists(atPath: fileURL.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if shared photo ready flag is set
|
* Check if shared photo ready flag is set
|
||||||
* This flag is set by the Share Extension when image is ready
|
* This flag is set by the Share Extension when image is ready
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers'
|
require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers'
|
||||||
|
|
||||||
platform :ios, '13.0'
|
platform :ios, '15.5'
|
||||||
use_frameworks!
|
use_frameworks!
|
||||||
|
|
||||||
# workaround to avoid Xcode caching of Pods that requires
|
# workaround to avoid Xcode caching of Pods that requires
|
||||||
@@ -30,9 +30,4 @@ end
|
|||||||
|
|
||||||
post_install do |installer|
|
post_install do |installer|
|
||||||
assertDeploymentTarget(installer)
|
assertDeploymentTarget(installer)
|
||||||
installer.pods_project.targets.each do |target|
|
|
||||||
target.build_configurations.each do |config|
|
|
||||||
config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,94 +1,84 @@
|
|||||||
PODS:
|
PODS:
|
||||||
- Capacitor (6.2.1):
|
- Capacitor (7.6.4):
|
||||||
- CapacitorCordova
|
- CapacitorCordova
|
||||||
- CapacitorApp (6.0.2):
|
- CapacitorApp (7.1.2):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
- CapacitorCamera (6.1.2):
|
- CapacitorCamera (7.0.5):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
- CapacitorClipboard (6.0.2):
|
- CapacitorClipboard (7.0.4):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
- CapacitorCommunitySqlite (6.0.2):
|
- CapacitorCommunitySqlite (7.0.3):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
- SQLCipher
|
- SQLCipher
|
||||||
- ZIPFoundation
|
- ZIPFoundation
|
||||||
- CapacitorCordova (6.2.1)
|
- CapacitorCordova (7.6.4)
|
||||||
- CapacitorFilesystem (6.0.3):
|
- CapacitorFilesystem (7.1.8):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
- CapacitorMlkitBarcodeScanning (6.2.0):
|
- IONFilesystemLib (~> 1.1.1)
|
||||||
|
- CapacitorMlkitBarcodeScanning (7.5.0):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
- GoogleMLKit/BarcodeScanning (= 5.0.0)
|
- GoogleMLKit/BarcodeScanning (= 7.0.0)
|
||||||
- CapacitorShare (6.0.3):
|
- CapacitorShare (7.0.4):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
- CapacitorStatusBar (6.0.2):
|
- CapacitorStatusBar (7.0.6):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
- CapawesomeCapacitorFilePicker (6.2.0):
|
- CapawesomeCapacitorFilePicker (7.2.0):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
- GoogleDataTransport (9.4.1):
|
- GoogleDataTransport (10.1.0):
|
||||||
- GoogleUtilities/Environment (~> 7.7)
|
- nanopb (~> 3.30910.0)
|
||||||
- nanopb (< 2.30911.0, >= 2.30908.0)
|
- PromisesObjC (~> 2.4)
|
||||||
- PromisesObjC (< 3.0, >= 1.2)
|
- GoogleMLKit/BarcodeScanning (7.0.0):
|
||||||
- GoogleMLKit/BarcodeScanning (5.0.0):
|
|
||||||
- GoogleMLKit/MLKitCore
|
- GoogleMLKit/MLKitCore
|
||||||
- MLKitBarcodeScanning (~> 4.0.0)
|
- MLKitBarcodeScanning (~> 6.0.0)
|
||||||
- GoogleMLKit/MLKitCore (5.0.0):
|
- GoogleMLKit/MLKitCore (7.0.0):
|
||||||
- MLKitCommon (~> 10.0.0)
|
- MLKitCommon (~> 12.0.0)
|
||||||
- GoogleToolboxForMac/DebugUtils (2.3.2):
|
- GoogleToolboxForMac/Defines (4.2.1)
|
||||||
- GoogleToolboxForMac/Defines (= 2.3.2)
|
- GoogleToolboxForMac/Logger (4.2.1):
|
||||||
- GoogleToolboxForMac/Defines (2.3.2)
|
- GoogleToolboxForMac/Defines (= 4.2.1)
|
||||||
- GoogleToolboxForMac/Logger (2.3.2):
|
- "GoogleToolboxForMac/NSData+zlib (4.2.1)":
|
||||||
- GoogleToolboxForMac/Defines (= 2.3.2)
|
- GoogleToolboxForMac/Defines (= 4.2.1)
|
||||||
- "GoogleToolboxForMac/NSData+zlib (2.3.2)":
|
- GoogleUtilities/Environment (8.1.0):
|
||||||
- GoogleToolboxForMac/Defines (= 2.3.2)
|
|
||||||
- "GoogleToolboxForMac/NSDictionary+URLArguments (2.3.2)":
|
|
||||||
- GoogleToolboxForMac/DebugUtils (= 2.3.2)
|
|
||||||
- GoogleToolboxForMac/Defines (= 2.3.2)
|
|
||||||
- "GoogleToolboxForMac/NSString+URLArguments (= 2.3.2)"
|
|
||||||
- "GoogleToolboxForMac/NSString+URLArguments (2.3.2)"
|
|
||||||
- GoogleUtilities/Environment (7.13.3):
|
|
||||||
- GoogleUtilities/Privacy
|
- GoogleUtilities/Privacy
|
||||||
- PromisesObjC (< 3.0, >= 1.2)
|
- GoogleUtilities/Logger (8.1.0):
|
||||||
- GoogleUtilities/Logger (7.13.3):
|
|
||||||
- GoogleUtilities/Environment
|
- GoogleUtilities/Environment
|
||||||
- GoogleUtilities/Privacy
|
- GoogleUtilities/Privacy
|
||||||
- GoogleUtilities/Privacy (7.13.3)
|
- GoogleUtilities/Privacy (8.1.0)
|
||||||
- GoogleUtilities/UserDefaults (7.13.3):
|
- GoogleUtilities/UserDefaults (8.1.0):
|
||||||
- GoogleUtilities/Logger
|
- GoogleUtilities/Logger
|
||||||
- GoogleUtilities/Privacy
|
- GoogleUtilities/Privacy
|
||||||
- GoogleUtilitiesComponents (1.1.0):
|
|
||||||
- GoogleUtilities/Logger
|
|
||||||
- GTMSessionFetcher/Core (3.5.0)
|
- GTMSessionFetcher/Core (3.5.0)
|
||||||
- MLImage (1.0.0-beta5)
|
- IONFilesystemLib (1.1.2)
|
||||||
- MLKitBarcodeScanning (4.0.0):
|
- MLImage (1.0.0-beta6)
|
||||||
- MLKitCommon (~> 10.0)
|
- MLKitBarcodeScanning (6.0.0):
|
||||||
- MLKitVision (~> 6.0)
|
- MLKitCommon (~> 12.0)
|
||||||
- MLKitCommon (10.0.0):
|
- MLKitVision (~> 8.0)
|
||||||
- GoogleDataTransport (~> 9.0)
|
- MLKitCommon (12.0.0):
|
||||||
- GoogleToolboxForMac/Logger (~> 2.1)
|
- GoogleDataTransport (~> 10.0)
|
||||||
- "GoogleToolboxForMac/NSData+zlib (~> 2.1)"
|
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
|
||||||
- "GoogleToolboxForMac/NSDictionary+URLArguments (~> 2.1)"
|
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
|
||||||
- GoogleUtilities/UserDefaults (~> 7.0)
|
- GoogleUtilities/Logger (~> 8.0)
|
||||||
- GoogleUtilitiesComponents (~> 1.0)
|
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||||
- GTMSessionFetcher/Core (< 4.0, >= 1.1)
|
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
|
||||||
- MLKitVision (6.0.0):
|
- MLKitVision (8.0.0):
|
||||||
- GoogleToolboxForMac/Logger (~> 2.1)
|
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
|
||||||
- "GoogleToolboxForMac/NSData+zlib (~> 2.1)"
|
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
|
||||||
- GTMSessionFetcher/Core (< 4.0, >= 1.1)
|
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
|
||||||
- MLImage (= 1.0.0-beta5)
|
- MLImage (= 1.0.0-beta6)
|
||||||
- MLKitCommon (~> 10.0)
|
- MLKitCommon (~> 12.0)
|
||||||
- nanopb (2.30910.0):
|
- nanopb (3.30910.0):
|
||||||
- nanopb/decode (= 2.30910.0)
|
- nanopb/decode (= 3.30910.0)
|
||||||
- nanopb/encode (= 2.30910.0)
|
- nanopb/encode (= 3.30910.0)
|
||||||
- nanopb/decode (2.30910.0)
|
- nanopb/decode (3.30910.0)
|
||||||
- nanopb/encode (2.30910.0)
|
- nanopb/encode (3.30910.0)
|
||||||
- PromisesObjC (2.4.0)
|
- PromisesObjC (2.4.0)
|
||||||
- SQLCipher (4.9.0):
|
- SQLCipher (4.10.0):
|
||||||
- SQLCipher/standard (= 4.9.0)
|
- SQLCipher/standard (= 4.10.0)
|
||||||
- SQLCipher/common (4.9.0)
|
- SQLCipher/common (4.10.0)
|
||||||
- SQLCipher/standard (4.9.0):
|
- SQLCipher/standard (4.10.0):
|
||||||
- SQLCipher/common
|
- SQLCipher/common
|
||||||
- TimesafariDailyNotificationPlugin (2.0.0):
|
- TimesafariDailyNotificationPlugin (4.0.1):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
- ZIPFoundation (0.9.19)
|
- ZIPFoundation (0.9.20)
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- "Capacitor (from `../../node_modules/@capacitor/ios`)"
|
- "Capacitor (from `../../node_modules/@capacitor/ios`)"
|
||||||
@@ -110,8 +100,8 @@ SPEC REPOS:
|
|||||||
- GoogleMLKit
|
- GoogleMLKit
|
||||||
- GoogleToolboxForMac
|
- GoogleToolboxForMac
|
||||||
- GoogleUtilities
|
- GoogleUtilities
|
||||||
- GoogleUtilitiesComponents
|
|
||||||
- GTMSessionFetcher
|
- GTMSessionFetcher
|
||||||
|
- IONFilesystemLib
|
||||||
- MLImage
|
- MLImage
|
||||||
- MLKitBarcodeScanning
|
- MLKitBarcodeScanning
|
||||||
- MLKitCommon
|
- MLKitCommon
|
||||||
@@ -148,33 +138,33 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../../node_modules/@timesafari/daily-notification-plugin"
|
:path: "../../node_modules/@timesafari/daily-notification-plugin"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
Capacitor: c95400d761e376be9da6be5a05f226c0e865cebf
|
Capacitor: 69dc07ebc6bd064747c5e76922f97e4862d9cc23
|
||||||
CapacitorApp: e1e6b7d05e444d593ca16fd6d76f2b7c48b5aea7
|
CapacitorApp: f01a913211780e0718dae9750442c3e23f96e106
|
||||||
CapacitorCamera: 9bc7b005d0e6f1d5f525b8137045b60cffffce79
|
CapacitorCamera: 9e952270be355797f769aa835bb7643a96c871fe
|
||||||
CapacitorClipboard: 4443c3cdb7c77b1533dfe3ff0f9f7756aa8579df
|
CapacitorClipboard: d1f123674cf413125db816a45e8f70e8770972fc
|
||||||
CapacitorCommunitySqlite: 0299d20f4b00c2e6aa485a1d8932656753937b9b
|
CapacitorCommunitySqlite: 4813d82ad33001e612a39d313cb5d28066cbafda
|
||||||
CapacitorCordova: 8d93e14982f440181be7304aa9559ca631d77fff
|
CapacitorCordova: e343e95a672ff73e21a77a80257b52fb609b47d5
|
||||||
CapacitorFilesystem: 59270a63c60836248812671aa3b15df673fbaf74
|
CapacitorFilesystem: c63fc54df41e5a6761785a7f3c49dc696c22e296
|
||||||
CapacitorMlkitBarcodeScanning: 7652be9c7922f39203a361de735d340ae37e134e
|
CapacitorMlkitBarcodeScanning: afd6fc431b550026a2c052e11ab2b71c7ae30011
|
||||||
CapacitorShare: d2a742baec21c8f3b92b361a2fbd2401cdd8288e
|
CapacitorShare: 25f7fc5dd0e4edbde5d6801c6de5d14a8b450a41
|
||||||
CapacitorStatusBar: b16799a26320ffa52f6c8b01737d5a95bbb8f3eb
|
CapacitorStatusBar: 416e9e53fd6397e668d4a181cd2131617d949bd6
|
||||||
CapawesomeCapacitorFilePicker: c40822f0a39f86855321943c7829d52bca7f01bd
|
CapawesomeCapacitorFilePicker: 0f4a913a00e39dd77213449f0d917e92f35a5ca9
|
||||||
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
|
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||||
GoogleMLKit: 90ba06e028795a50261f29500d238d6061538711
|
GoogleMLKit: eff9e23ec1d90ea4157a1ee2e32a4f610c5b3318
|
||||||
GoogleToolboxForMac: 8bef7c7c5cf7291c687cf5354f39f9db6399ad34
|
GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8
|
||||||
GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
|
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||||
GoogleUtilitiesComponents: 679b2c881db3b615a2777504623df6122dd20afe
|
|
||||||
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
|
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
|
||||||
MLImage: 1824212150da33ef225fbd3dc49f184cf611046c
|
IONFilesystemLib: 21a63377696b2d8fab5632ecfb7d2ac67bddb68a
|
||||||
MLKitBarcodeScanning: 9cb0ec5ec65bbb5db31de4eba0a3289626beab4e
|
MLImage: 0ad1c5f50edd027672d8b26b0fee78a8b4a0fc56
|
||||||
MLKitCommon: afcd11b6c0735066a0dde8b4bf2331f6197cbca2
|
MLKitBarcodeScanning: 0a3064da0a7f49ac24ceb3cb46a5bc67496facd2
|
||||||
MLKitVision: 90922bca854014a856f8b649d1f1f04f63fd9c79
|
MLKitCommon: 07c2c33ae5640e5380beaaa6e4b9c249a205542d
|
||||||
nanopb: 438bc412db1928dac798aa6fd75726007be04262
|
MLKitVision: 45e79d68845a2de77e2dd4d7f07947f0ed157b0e
|
||||||
|
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||||
SQLCipher: 31878d8ebd27e5c96db0b7cb695c96e9f8ad77da
|
SQLCipher: eb79c64049cb002b4e9fcb30edb7979bf4706dfc
|
||||||
TimesafariDailyNotificationPlugin: 3c12e8c39fc27f689f56cf4e57230a8c28611fcc
|
TimesafariDailyNotificationPlugin: 69277c884380a9a620f671b68e0327eaa4b3d27d
|
||||||
ZIPFoundation: b8c29ea7ae353b309bc810586181fd073cb3312c
|
ZIPFoundation: dfd3d681c4053ff7e2f7350bc4e53b5dba3f5351
|
||||||
|
|
||||||
PODFILE CHECKSUM: 6d92bfa46c6c2d31d19b8c0c38f56a8ae9fd222f
|
PODFILE CHECKSUM: 87c07d03f36ef38ab0c873802aee1ce9b5d34448
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
@@ -9,135 +9,200 @@ import UIKit
|
|||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
class ShareViewController: UIViewController {
|
class ShareViewController: UIViewController {
|
||||||
|
|
||||||
private let appGroupIdentifier = "group.app.timesafari.share"
|
private let appGroupIdentifier = "group.app.timesafari.share"
|
||||||
private let sharedPhotoFileNameKey = "sharedPhotoFileName"
|
private let sharedPhotoFileNameKey = "sharedPhotoFileName"
|
||||||
private let sharedPhotoFilePathKey = "sharedPhotoFilePath"
|
private let sharedPhotoFilePathKey = "sharedPhotoFilePath"
|
||||||
|
private let sharedPhotoShareIdKey = "sharedPhotoShareId"
|
||||||
|
private let shareExtensionLastStartKey = "shareExtensionLastStart"
|
||||||
private let sharedImageFileName = "shared-image"
|
private let sharedImageFileName = "shared-image"
|
||||||
|
|
||||||
/// Get the App Group container URL for storing shared files
|
/// Get the App Group container URL for storing shared files
|
||||||
private var appGroupContainerURL: URL? {
|
private var appGroupContainerURL: URL? {
|
||||||
return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)
|
return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
|
if let userDefaults = UserDefaults(suiteName: appGroupIdentifier) {
|
||||||
|
let timestamp = ISO8601DateFormatter().string(from: Date())
|
||||||
|
userDefaults.set(timestamp, forKey: shareExtensionLastStartKey)
|
||||||
|
userDefaults.synchronize()
|
||||||
|
print("[ShareTarget] shareExtensionLastStart=\(timestamp)")
|
||||||
|
}
|
||||||
|
|
||||||
|
print("[ShareTarget] viewDidLoad started")
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
// Set a minimal background (transparent or loading indicator)
|
// Set a minimal background (transparent or loading indicator)
|
||||||
view.backgroundColor = .systemBackground
|
view.backgroundColor = .systemBackground
|
||||||
|
|
||||||
// Process image immediately without showing UI
|
// Process image immediately without showing UI
|
||||||
processAndOpenApp()
|
processAndOpenApp()
|
||||||
|
print("[ShareTarget] viewDidLoad completed")
|
||||||
}
|
}
|
||||||
|
|
||||||
private func processAndOpenApp() {
|
private func processAndOpenApp() {
|
||||||
|
print("[ShareTarget] processAndOpenApp started")
|
||||||
|
|
||||||
// extensionContext is automatically available on UIViewController when used as extension principal class
|
// extensionContext is automatically available on UIViewController when used as extension principal class
|
||||||
guard let context = extensionContext,
|
guard let context = extensionContext,
|
||||||
let inputItems = context.inputItems as? [NSExtensionItem] else {
|
let inputItems = context.inputItems as? [NSExtensionItem] else {
|
||||||
|
print("[ShareTarget] processAndOpenApp failed: missing extensionContext or inputItems")
|
||||||
|
print("[ShareTarget] completeRequest starting")
|
||||||
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
|
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
|
||||||
|
print("[ShareTarget] completeRequest completed")
|
||||||
|
print("[ShareTarget] processAndOpenApp completed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let attachmentCount = inputItems.reduce(0) { count, item in
|
||||||
|
count + (item.attachments?.count ?? 0)
|
||||||
|
}
|
||||||
|
print("[ShareTarget] processAndOpenApp inputItems=\(inputItems.count) attachmentCount=\(attachmentCount)")
|
||||||
|
|
||||||
processSharedImage(from: inputItems) { [weak self] success in
|
processSharedImage(from: inputItems) { [weak self] success in
|
||||||
guard let self = self, let context = self.extensionContext else {
|
guard let self = self, let context = self.extensionContext else {
|
||||||
|
print("[ShareTarget] processAndOpenApp failed: self or extensionContext unavailable in completion")
|
||||||
|
print("[ShareTarget] processAndOpenApp completed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if success {
|
if success {
|
||||||
// Set flag that shared photo is ready
|
// Set flag that shared photo is ready
|
||||||
self.setSharedPhotoReadyFlag()
|
self.setSharedPhotoReadyFlag()
|
||||||
// Open the main app (using minimal URL - app will detect shared data on activation)
|
// Open the main app (using minimal URL - app will detect shared data on activation)
|
||||||
self.openMainApp()
|
self.openMainApp()
|
||||||
|
} else {
|
||||||
|
print("[ShareTarget] processAndOpenApp failed: processSharedImage returned false")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Complete immediately - no UI shown
|
// Complete immediately - no UI shown
|
||||||
|
print("[ShareTarget] completeRequest starting")
|
||||||
context.completeRequest(returningItems: [], completionHandler: nil)
|
context.completeRequest(returningItems: [], completionHandler: nil)
|
||||||
|
print("[ShareTarget] completeRequest completed")
|
||||||
|
print("[ShareTarget] processAndOpenApp completed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setSharedPhotoReadyFlag() {
|
private func setSharedPhotoReadyFlag() {
|
||||||
|
print("[ShareTarget] setSharedPhotoReadyFlag started")
|
||||||
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
|
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
|
||||||
|
print("[ShareTarget] setSharedPhotoReadyFlag failed: UserDefaults unavailable for app group")
|
||||||
|
print("[ShareTarget] setSharedPhotoReadyFlag completed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
userDefaults.set(true, forKey: "sharedPhotoReady")
|
userDefaults.set(true, forKey: "sharedPhotoReady")
|
||||||
userDefaults.synchronize()
|
userDefaults.synchronize()
|
||||||
|
print("[ShareTarget] setSharedPhotoReadyFlag success")
|
||||||
|
print("[ShareTarget] setSharedPhotoReadyFlag completed")
|
||||||
}
|
}
|
||||||
|
|
||||||
private func processSharedImage(from items: [NSExtensionItem], completion: @escaping (Bool) -> Void) {
|
private func processSharedImage(from items: [NSExtensionItem], completion: @escaping (Bool) -> Void) {
|
||||||
|
let attachmentCount = items.reduce(0) { count, item in
|
||||||
|
count + (item.attachments?.count ?? 0)
|
||||||
|
}
|
||||||
|
print("[ShareTarget] processSharedImage started attachmentCount=\(attachmentCount)")
|
||||||
|
|
||||||
// Find the first image attachment
|
// Find the first image attachment
|
||||||
for item in items {
|
for item in items {
|
||||||
guard let attachments = item.attachments else {
|
guard let attachments = item.attachments else {
|
||||||
|
print("[ShareTarget] processSharedImage skipping item with no attachments")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
for attachment in attachments {
|
for attachment in attachments {
|
||||||
// Skip non-image attachments
|
// Skip non-image attachments
|
||||||
guard attachment.hasItemConformingToTypeIdentifier(UTType.image.identifier) else {
|
guard attachment.hasItemConformingToTypeIdentifier(UTType.image.identifier) else {
|
||||||
|
print("[ShareTarget] processSharedImage skipping non-image attachment")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let shareId = UUID().uuidString
|
||||||
|
print("[ShareTarget] processSharedImage found image attachment shareId=\(shareId) UTType=\(UTType.image.identifier)")
|
||||||
|
print("[ShareTarget] share received shareId=\(shareId)")
|
||||||
|
|
||||||
// Try to load raw data first to preserve original format
|
// Try to load raw data first to preserve original format
|
||||||
// This preserves the original image format without conversion
|
// This preserves the original image format without conversion
|
||||||
attachment.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { [weak self] (data, error) in
|
attachment.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { [weak self] (data, error) in
|
||||||
guard let self = self else {
|
guard let self = self else {
|
||||||
completion(false)
|
print("[ShareTarget] processSharedImage failed: self unavailable in loadItem callback shareId=\(shareId)")
|
||||||
return
|
print("[ShareTarget] processSharedImage completed shareId=\(shareId) success=false")
|
||||||
}
|
completion(false)
|
||||||
|
return
|
||||||
if error != nil {
|
|
||||||
completion(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle different image data types
|
|
||||||
// loadItem(forTypeIdentifier:) typically returns URL or Data, not UIImage
|
|
||||||
var imageData: Data?
|
|
||||||
var fileName: String = "shared-image"
|
|
||||||
|
|
||||||
if let url = data as? URL {
|
|
||||||
// Most common case: Image provided as file URL - read raw data to preserve format
|
|
||||||
let accessing = url.startAccessingSecurityScopedResource()
|
|
||||||
defer {
|
|
||||||
if accessing {
|
|
||||||
url.stopAccessingSecurityScopedResource()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read raw data directly to preserve original format
|
|
||||||
imageData = try? Data(contentsOf: url)
|
|
||||||
fileName = url.lastPathComponent
|
|
||||||
|
|
||||||
// Fallback: if raw data read fails, try UIImage and convert to PNG (lossless)
|
|
||||||
if imageData == nil, let image = UIImage(contentsOfFile: url.path) {
|
|
||||||
imageData = image.pngData()
|
|
||||||
fileName = self.getFileNameWithExtension(url.lastPathComponent, newExtension: "png")
|
|
||||||
}
|
|
||||||
} else if let data = data as? Data {
|
|
||||||
// Less common: Image provided as raw Data - use directly to preserve format
|
|
||||||
imageData = data
|
|
||||||
fileName = attachment.suggestedName ?? "shared-image"
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let finalImageData = imageData else {
|
|
||||||
completion(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store image as file in App Group container
|
|
||||||
if self.storeImageData(finalImageData, fileName: fileName) {
|
|
||||||
completion(true)
|
|
||||||
} else {
|
|
||||||
completion(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return // Process only the first image
|
|
||||||
|
if let error = error {
|
||||||
|
print("[ShareTarget] processSharedImage failed: loadItem error shareId=\(shareId) error=\(error.localizedDescription)")
|
||||||
|
print("[ShareTarget] processSharedImage completed shareId=\(shareId) success=false")
|
||||||
|
completion(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle different image data types
|
||||||
|
// loadItem(forTypeIdentifier:) typically returns URL or Data, not UIImage
|
||||||
|
var imageData: Data?
|
||||||
|
var fileName: String = "shared-image"
|
||||||
|
|
||||||
|
if let url = data as? URL {
|
||||||
|
print("[ShareTarget] processSharedImage loadItem returned URL shareId=\(shareId) url=\(url.lastPathComponent)")
|
||||||
|
// Most common case: Image provided as file URL - read raw data to preserve format
|
||||||
|
let accessing = url.startAccessingSecurityScopedResource()
|
||||||
|
defer {
|
||||||
|
if accessing {
|
||||||
|
url.stopAccessingSecurityScopedResource()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read raw data directly to preserve original format
|
||||||
|
imageData = try? Data(contentsOf: url)
|
||||||
|
fileName = url.lastPathComponent
|
||||||
|
|
||||||
|
// Fallback: if raw data read fails, try UIImage and convert to PNG (lossless)
|
||||||
|
if imageData == nil, let image = UIImage(contentsOfFile: url.path) {
|
||||||
|
print("[ShareTarget] processSharedImage using UIImage PNG fallback shareId=\(shareId)")
|
||||||
|
imageData = image.pngData()
|
||||||
|
fileName = self.getFileNameWithExtension(url.lastPathComponent, newExtension: "png")
|
||||||
|
} else if imageData == nil {
|
||||||
|
print("[ShareTarget] processSharedImage failed: could not read image data from URL shareId=\(shareId)")
|
||||||
|
}
|
||||||
|
} else if let data = data as? Data {
|
||||||
|
print("[ShareTarget] processSharedImage loadItem returned Data shareId=\(shareId)")
|
||||||
|
// Less common: Image provided as raw Data - use directly to preserve format
|
||||||
|
imageData = data
|
||||||
|
fileName = attachment.suggestedName ?? "shared-image"
|
||||||
|
} else {
|
||||||
|
print("[ShareTarget] processSharedImage failed: loadItem returned unexpected type shareId=\(shareId) type=\(String(describing: type(of: data)))")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let finalImageData = imageData else {
|
||||||
|
print("[ShareTarget] processSharedImage failed: no image data shareId=\(shareId)")
|
||||||
|
print("[ShareTarget] processSharedImage completed shareId=\(shareId) success=false")
|
||||||
|
completion(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
print("[ShareTarget] image loaded bytes=\(finalImageData.count) filename=\(fileName) shareId=\(shareId)")
|
||||||
|
|
||||||
|
// Store image as file in App Group container
|
||||||
|
if self.storeImageData(finalImageData, fileName: fileName, shareId: shareId) {
|
||||||
|
print("[ShareTarget] processSharedImage completed shareId=\(shareId) success=true")
|
||||||
|
completion(true)
|
||||||
|
} else {
|
||||||
|
print("[ShareTarget] processSharedImage failed: storeImageData returned false shareId=\(shareId)")
|
||||||
|
print("[ShareTarget] processSharedImage completed shareId=\(shareId) success=false")
|
||||||
|
completion(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return // Process only the first image
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// No image found
|
// No image found
|
||||||
|
print("[ShareTarget] processSharedImage failed: no image attachment found attachmentCount=\(attachmentCount)")
|
||||||
|
print("[ShareTarget] processSharedImage completed success=false")
|
||||||
completion(false)
|
completion(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper to get filename with a new extension, preserving base name
|
/// Helper to get filename with a new extension, preserving base name
|
||||||
private func getFileNameWithExtension(_ originalName: String, newExtension: String) -> String {
|
private func getFileNameWithExtension(_ originalName: String, newExtension: String) -> String {
|
||||||
if let nameWithoutExt = originalName.components(separatedBy: ".").first, !nameWithoutExt.isEmpty {
|
if let nameWithoutExt = originalName.components(separatedBy: ".").first, !nameWithoutExt.isEmpty {
|
||||||
@@ -145,63 +210,98 @@ class ShareViewController: UIViewController {
|
|||||||
}
|
}
|
||||||
return "shared-image.\(newExtension)"
|
return "shared-image.\(newExtension)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extract file extension from original filename, defaulting to jpg when absent
|
||||||
|
private func fileExtension(from fileName: String) -> String {
|
||||||
|
let ext = (fileName as NSString).pathExtension
|
||||||
|
return ext.isEmpty ? "jpg" : ext.lowercased()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build unique on-disk filename: <shareId>.<extension>
|
||||||
|
private func storedFileName(shareId: String, originalFileName: String) -> String {
|
||||||
|
return "\(shareId).\(fileExtension(from: originalFileName))"
|
||||||
|
}
|
||||||
|
|
||||||
/// Store image data as a file in the App Group container
|
/// Store image data as a file in the App Group container
|
||||||
/// All images are stored as files regardless of size for consistency and simplicity
|
/// All images are stored as files regardless of size for consistency and simplicity
|
||||||
/// Returns true if successful, false otherwise
|
/// Returns true if successful, false otherwise
|
||||||
private func storeImageData(_ imageData: Data, fileName: String) -> Bool {
|
private func storeImageData(_ imageData: Data, fileName: String, shareId: String) -> Bool {
|
||||||
|
print("[ShareTarget] storeImageData started shareId=\(shareId) bytes=\(imageData.count) filename=\(fileName)")
|
||||||
|
|
||||||
guard let containerURL = appGroupContainerURL else {
|
guard let containerURL = appGroupContainerURL else {
|
||||||
|
print("[ShareTarget] storeImageData failed: app group container unavailable shareId=\(shareId)")
|
||||||
|
print("[ShareTarget] storeImageData completed shareId=\(shareId) success=false")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create file URL in the container using the actual filename
|
let originalFileName = fileName.isEmpty ? "\(sharedImageFileName).jpg" : fileName
|
||||||
// Extract extension from fileName if present, otherwise use sharedImageFileName
|
let storedFileName = storedFileName(shareId: shareId, originalFileName: originalFileName)
|
||||||
let actualFileName = fileName.isEmpty ? sharedImageFileName : fileName
|
let fileURL = containerURL.appendingPathComponent(storedFileName)
|
||||||
let fileURL = containerURL.appendingPathComponent(actualFileName)
|
|
||||||
|
// Remove previously pending share file (metadata tracks one share at a time)
|
||||||
// Remove old file if it exists
|
if let userDefaults = UserDefaults(suiteName: appGroupIdentifier),
|
||||||
try? FileManager.default.removeItem(at: fileURL)
|
let previousPath = userDefaults.string(forKey: sharedPhotoFilePathKey) {
|
||||||
|
let previousURL = containerURL.appendingPathComponent(previousPath)
|
||||||
|
if previousURL != fileURL {
|
||||||
|
try? FileManager.default.removeItem(at: previousURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Write image data to file
|
// Write image data to file
|
||||||
do {
|
do {
|
||||||
try imageData.write(to: fileURL)
|
try imageData.write(to: fileURL)
|
||||||
} catch {
|
} catch {
|
||||||
|
print("[ShareTarget] storeImageData failed: file write error shareId=\(shareId) error=\(error.localizedDescription)")
|
||||||
|
print("[ShareTarget] storeImageData completed shareId=\(shareId) success=false")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
print("[ShareTarget] file stored shareId=\(shareId) originalFilename=\(originalFileName) storedFilename=\(storedFileName)")
|
||||||
|
|
||||||
// Store file path and filename in UserDefaults (small data, safe to store)
|
// Store file path and filename in UserDefaults (small data, safe to store)
|
||||||
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
|
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
|
||||||
|
print("[ShareTarget] storeImageData failed: UserDefaults unavailable shareId=\(shareId)")
|
||||||
|
print("[ShareTarget] storeImageData completed shareId=\(shareId) success=false")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store relative path and filename
|
// sharedPhotoFilePath = on-disk name; sharedPhotoFileName = original display name
|
||||||
userDefaults.set(actualFileName, forKey: sharedPhotoFilePathKey)
|
userDefaults.set(storedFileName, forKey: sharedPhotoFilePathKey)
|
||||||
userDefaults.set(fileName, forKey: sharedPhotoFileNameKey)
|
userDefaults.set(originalFileName, forKey: sharedPhotoFileNameKey)
|
||||||
|
userDefaults.set(shareId, forKey: sharedPhotoShareIdKey)
|
||||||
|
|
||||||
// Clean up any old base64 data that might exist
|
// Clean up any old base64 data that might exist
|
||||||
userDefaults.removeObject(forKey: "sharedPhotoBase64")
|
userDefaults.removeObject(forKey: "sharedPhotoBase64")
|
||||||
|
|
||||||
userDefaults.synchronize()
|
userDefaults.synchronize()
|
||||||
|
print("[ShareTarget] metadata stored shareId=\(shareId) originalFilename=\(originalFileName) storedFilename=\(storedFileName)")
|
||||||
|
print("[ShareTarget] storeImageData success shareId=\(shareId)")
|
||||||
|
print("[ShareTarget] storeImageData completed shareId=\(shareId) success=true")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private func openMainApp() {
|
private func openMainApp() {
|
||||||
|
print("[ShareTarget] openMainApp starting")
|
||||||
|
|
||||||
// Open the main app with minimal URL - app will detect shared data on activation
|
// Open the main app with minimal URL - app will detect shared data on activation
|
||||||
guard let url = URL(string: "timesafari://") else {
|
guard let url = URL(string: "timesafari://") else {
|
||||||
|
print("[ShareTarget] openMainApp failed: could not create timesafari:// URL")
|
||||||
|
print("[ShareTarget] openMainApp completed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var responder: UIResponder? = self
|
var responder: UIResponder? = self
|
||||||
while responder != nil {
|
while responder != nil {
|
||||||
if let application = responder as? UIApplication {
|
if let application = responder as? UIApplication {
|
||||||
application.open(url, options: [:], completionHandler: nil)
|
application.open(url, options: [:], completionHandler: nil)
|
||||||
|
print("[ShareTarget] openMainApp completed via UIApplication")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
responder = responder?.next
|
responder = responder?.next
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: use extension context
|
// Fallback: use extension context
|
||||||
extensionContext?.open(url, completionHandler: nil)
|
extensionContext?.open(url, completionHandler: nil)
|
||||||
|
print("[ShareTarget] openMainApp completed via extensionContext fallback")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
8694
package-lock.json
generated
8694
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
32
package.json
32
package.json
@@ -1,6 +1,7 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
"name": "giftopia",
|
"name": "giftopia",
|
||||||
"version": "1.3.13-beta",
|
"version": "1.4.3",
|
||||||
"description": "Giftopia App",
|
"description": "Giftopia App",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Gift Economies Team"
|
"name": "Gift Economies Team"
|
||||||
@@ -13,6 +14,7 @@
|
|||||||
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js && node scripts/copy-wasm.js",
|
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js && node scripts/copy-wasm.js",
|
||||||
"test:prerequisites": "node scripts/check-prerequisites.js",
|
"test:prerequisites": "node scripts/check-prerequisites.js",
|
||||||
"check:dependencies": "./scripts/check-dependencies.sh",
|
"check:dependencies": "./scripts/check-dependencies.sh",
|
||||||
|
"deps:update-daily-notification-plugin": "npm install @timesafari/daily-notification-plugin@git+https://gitea.anomalistdesign.com/trent_larson/daily-notification-plugin.git#master",
|
||||||
"test:all": "npm run lint && tsc && npm run test:web && npm run test:mobile && ./scripts/test-safety-check.sh && echo '\n\n\nGotta add the performance tests'",
|
"test:all": "npm run lint && tsc && npm run test:web && npm run test:mobile && ./scripts/test-safety-check.sh && echo '\n\n\nGotta add the performance tests'",
|
||||||
"test:web": "npx playwright test -c playwright.config-local.ts --trace on",
|
"test:web": "npx playwright test -c playwright.config-local.ts --trace on",
|
||||||
"test:mobile": "./scripts/test-mobile.sh",
|
"test:mobile": "./scripts/test-mobile.sh",
|
||||||
@@ -28,7 +30,7 @@
|
|||||||
"auto-run:electron": "./scripts/auto-run.sh --platform=electron",
|
"auto-run:electron": "./scripts/auto-run.sh --platform=electron",
|
||||||
"build:capacitor": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --mode capacitor --config vite.config.capacitor.mts",
|
"build:capacitor": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --mode capacitor --config vite.config.capacitor.mts",
|
||||||
"build:capacitor:sync": "npm run build:capacitor && npx cap sync && node scripts/restore-local-plugins.js",
|
"build:capacitor:sync": "npm run build:capacitor && npx cap sync && node scripts/restore-local-plugins.js",
|
||||||
"build:native": "vite build && npx cap sync && node scripts/restore-local-plugins.js && npx capacitor-assets generate",
|
"build:native": "vite build && npx cap sync && node scripts/restore-local-plugins.js && bash -c 'source scripts/common.sh && ensure_ios_capacitor_asset_directories' && npx capacitor-assets generate",
|
||||||
"assets:config": "npx tsx scripts/assets-config.ts",
|
"assets:config": "npx tsx scripts/assets-config.ts",
|
||||||
"assets:validate": "npx tsx scripts/assets-validator.ts",
|
"assets:validate": "npx tsx scripts/assets-validator.ts",
|
||||||
"assets:validate:android": "./scripts/build-android.sh --assets-only",
|
"assets:validate:android": "./scripts/build-android.sh --assets-only",
|
||||||
@@ -138,19 +140,19 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor-community/electron": "^5.0.1",
|
"@capacitor-community/electron": "^5.0.1",
|
||||||
"@capacitor-community/sqlite": "6.0.2",
|
"@capacitor-community/sqlite": "^7.0.3",
|
||||||
"@capacitor-mlkit/barcode-scanning": "^6.0.0",
|
"@capacitor-mlkit/barcode-scanning": "^7.5.0",
|
||||||
"@capacitor/android": "^6.2.0",
|
"@capacitor/android": "^7.6.4",
|
||||||
"@capacitor/app": "^6.0.0",
|
"@capacitor/app": "^7.1.0",
|
||||||
"@capacitor/camera": "^6.0.0",
|
"@capacitor/camera": "^7.0.5",
|
||||||
"@capacitor/cli": "^6.2.0",
|
"@capacitor/cli": "^7.6.4",
|
||||||
"@capacitor/clipboard": "^6.0.2",
|
"@capacitor/clipboard": "^7.0.4",
|
||||||
"@capacitor/core": "^6.2.0",
|
"@capacitor/core": "^7.6.4",
|
||||||
"@capacitor/filesystem": "^6.0.0",
|
"@capacitor/filesystem": "^7.1.8",
|
||||||
"@capacitor/ios": "^6.2.0",
|
"@capacitor/ios": "^7.6.4",
|
||||||
"@capacitor/share": "^6.0.3",
|
"@capacitor/share": "^7.0.4",
|
||||||
"@capacitor/status-bar": "^6.0.2",
|
"@capacitor/status-bar": "^7.0.6",
|
||||||
"@capawesome/capacitor-file-picker": "^6.2.0",
|
"@capawesome/capacitor-file-picker": "^7.2.0",
|
||||||
"@dicebear/collection": "^5.4.1",
|
"@dicebear/collection": "^5.4.1",
|
||||||
"@dicebear/core": "^5.4.1",
|
"@dicebear/core": "^5.4.1",
|
||||||
"@ethersproject/hdnode": "^5.7.0",
|
"@ethersproject/hdnode": "^5.7.0",
|
||||||
|
|||||||
@@ -222,7 +222,9 @@ build_ios_app() {
|
|||||||
|
|
||||||
if [ "$BUILD_TYPE" = "debug" ]; then
|
if [ "$BUILD_TYPE" = "debug" ]; then
|
||||||
build_config="Debug"
|
build_config="Debug"
|
||||||
destination="platform=iOS Simulator,name=iPhone 15 Pro"
|
# Use device SDK — prebuilt MLKit frameworks (MLImage, MLKitBarcodeScanning) ship
|
||||||
|
# iOS device slices only and cannot link against the iOS Simulator SDK.
|
||||||
|
destination="generic/platform=iOS"
|
||||||
else
|
else
|
||||||
build_config="Release"
|
build_config="Release"
|
||||||
destination="platform=iOS,id=auto"
|
destination="platform=iOS,id=auto"
|
||||||
@@ -232,18 +234,34 @@ build_ios_app() {
|
|||||||
|
|
||||||
cd ios/App
|
cd ios/App
|
||||||
|
|
||||||
# Build the app
|
# Prevent pkgx-managed libs (e.g. zlib) from leaking into the iOS SDK linker.
|
||||||
xcodebuild -workspace App.xcworkspace \
|
unset LIBRARY_PATH
|
||||||
|
unset DYLD_LIBRARY_PATH
|
||||||
|
unset DYLD_FALLBACK_LIBRARY_PATH
|
||||||
|
|
||||||
|
# Build the app:
|
||||||
|
# -quiet: skip the huge export VAR dump (compiler warnings still show unless suppressed below).
|
||||||
|
# SWIFT_SUPPRESS_WARNINGS / GCC_WARN_INHIBIT_ALL_WARNINGS: quiet CLI output from Pods + plugins;
|
||||||
|
# build in Xcode for full diagnostics. Real errors still fail the build.
|
||||||
|
local build_exit=0
|
||||||
|
xcodebuild -quiet \
|
||||||
|
-workspace App.xcworkspace \
|
||||||
-scheme "$scheme" \
|
-scheme "$scheme" \
|
||||||
-configuration "$build_config" \
|
-configuration "$build_config" \
|
||||||
-destination "$destination" \
|
-destination "$destination" \
|
||||||
build \
|
build \
|
||||||
CODE_SIGN_IDENTITY="" \
|
CODE_SIGN_IDENTITY="" \
|
||||||
CODE_SIGNING_REQUIRED=NO \
|
CODE_SIGNING_REQUIRED=NO \
|
||||||
CODE_SIGNING_ALLOWED=NO
|
CODE_SIGNING_ALLOWED=NO \
|
||||||
|
SWIFT_SUPPRESS_WARNINGS=YES \
|
||||||
|
GCC_WARN_INHIBIT_ALL_WARNINGS=YES || build_exit=$?
|
||||||
|
|
||||||
cd ../..
|
cd ../..
|
||||||
|
|
||||||
|
if [ $build_exit -ne 0 ]; then
|
||||||
|
return $build_exit
|
||||||
|
fi
|
||||||
|
|
||||||
log_success "iOS app built successfully"
|
log_success "iOS app built successfully"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,6 +424,7 @@ fi
|
|||||||
# Handle assets-only mode
|
# Handle assets-only mode
|
||||||
if [ "$ASSETS_ONLY" = true ]; then
|
if [ "$ASSETS_ONLY" = true ]; then
|
||||||
log_info "Assets-only mode: generating assets"
|
log_info "Assets-only mode: generating assets"
|
||||||
|
ensure_ios_capacitor_asset_directories
|
||||||
safe_execute "Generating assets" "npx capacitor-assets generate --ios" || exit 7
|
safe_execute "Generating assets" "npx capacitor-assets generate --ios" || exit 7
|
||||||
log_success "Assets generation completed successfully!"
|
log_success "Assets generation completed successfully!"
|
||||||
exit 0
|
exit 0
|
||||||
@@ -555,6 +574,7 @@ safe_execute "Installing CocoaPods dependencies" "run_pod_install_with_workaroun
|
|||||||
safe_execute "Syncing with Capacitor" "run_cap_sync_with_workaround" || exit 6
|
safe_execute "Syncing with Capacitor" "run_cap_sync_with_workaround" || exit 6
|
||||||
|
|
||||||
# Step 7: Generate assets
|
# Step 7: Generate assets
|
||||||
|
ensure_ios_capacitor_asset_directories
|
||||||
safe_execute "Generating assets" "npx capacitor-assets generate --ios" || exit 7
|
safe_execute "Generating assets" "npx capacitor-assets generate --ios" || exit 7
|
||||||
|
|
||||||
# Step 8: Build iOS app
|
# Step 8: Build iOS app
|
||||||
@@ -564,16 +584,19 @@ safe_execute "Building iOS app" "build_ios_app" || exit 5
|
|||||||
if [ "$BUILD_IPA" = true ]; then
|
if [ "$BUILD_IPA" = true ]; then
|
||||||
log_info "Building IPA package..."
|
log_info "Building IPA package..."
|
||||||
cd ios/App
|
cd ios/App
|
||||||
xcodebuild -workspace App.xcworkspace \
|
xcodebuild -quiet \
|
||||||
|
-workspace App.xcworkspace \
|
||||||
-scheme App \
|
-scheme App \
|
||||||
-configuration Release \
|
-configuration Release \
|
||||||
-archivePath build/App.xcarchive \
|
-archivePath build/App.xcarchive \
|
||||||
archive \
|
archive \
|
||||||
CODE_SIGN_IDENTITY="" \
|
CODE_SIGN_IDENTITY="" \
|
||||||
CODE_SIGNING_REQUIRED=NO \
|
CODE_SIGNING_REQUIRED=NO \
|
||||||
CODE_SIGNING_ALLOWED=NO
|
CODE_SIGNING_ALLOWED=NO \
|
||||||
|
SWIFT_SUPPRESS_WARNINGS=YES \
|
||||||
|
GCC_WARN_INHIBIT_ALL_WARNINGS=YES
|
||||||
|
|
||||||
xcodebuild -exportArchive \
|
xcodebuild -quiet -exportArchive \
|
||||||
-archivePath build/App.xcarchive \
|
-archivePath build/App.xcarchive \
|
||||||
-exportPath build/ \
|
-exportPath build/ \
|
||||||
-exportOptionsPlist exportOptions.plist
|
-exportOptionsPlist exportOptions.plist
|
||||||
|
|||||||
@@ -337,6 +337,27 @@ parse_args() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# iOS: capacitor-assets writes into AppIcon.appiconset and Splash.imageset under
|
||||||
|
# Assets.xcassets. Those paths are gitignored (generated). On a fresh clone the
|
||||||
|
# folders and Contents.json are missing; the tool opens Contents.json before writing
|
||||||
|
# PNGs, so we create minimal asset-catalog stubs when absent.
|
||||||
|
ensure_ios_capacitor_asset_directories() {
|
||||||
|
local base="ios/App/App/Assets.xcassets"
|
||||||
|
if [ ! -d "$base" ]; then
|
||||||
|
log_warn "Missing $base — cannot prepare iOS asset directories"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
mkdir -p "$base/AppIcon.appiconset" "$base/Splash.imageset"
|
||||||
|
local minimal_contents='{"images":[],"info":{"author":"xcode","version":1}}'
|
||||||
|
if [ ! -f "$base/AppIcon.appiconset/Contents.json" ]; then
|
||||||
|
printf '%s\n' "$minimal_contents" > "$base/AppIcon.appiconset/Contents.json"
|
||||||
|
fi
|
||||||
|
if [ ! -f "$base/Splash.imageset/Contents.json" ]; then
|
||||||
|
printf '%s\n' "$minimal_contents" > "$base/Splash.imageset/Contents.json"
|
||||||
|
fi
|
||||||
|
log_debug "Ensured iOS capacitor-assets output directories exist"
|
||||||
|
}
|
||||||
|
|
||||||
# Export functions for use in child scripts
|
# Export functions for use in child scripts
|
||||||
export -f log_info log_success log_warn log_error log_debug log_step
|
export -f log_info log_success log_warn log_error log_debug log_step
|
||||||
export -f measure_time print_header print_footer
|
export -f measure_time print_header print_footer
|
||||||
@@ -344,4 +365,5 @@ export -f check_command check_directory check_file
|
|||||||
export -f safe_execute check_venv get_git_hash
|
export -f safe_execute check_venv get_git_hash
|
||||||
export -f clean_build_artifacts validate_env_vars
|
export -f clean_build_artifacts validate_env_vars
|
||||||
export -f setup_build_env setup_app_directories load_env_file print_env_vars
|
export -f setup_build_env setup_app_directories load_env_file print_env_vars
|
||||||
export -f print_usage parse_args
|
export -f print_usage parse_args
|
||||||
|
export -f ensure_ios_capacitor_asset_directories
|
||||||
@@ -8,14 +8,14 @@ notifications for conflicted entities * - Template streamlined with computed CSS
|
|||||||
properties * * @author Matthew Raymer */
|
properties * * @author Matthew Raymer */
|
||||||
<template>
|
<template>
|
||||||
<div id="sectionGiftedGiver">
|
<div id="sectionGiftedGiver">
|
||||||
<label class="block font-bold mb-1">
|
<label class="block font-semibold text-lg capitalize text-center">
|
||||||
{{ stepLabel }}
|
{{ stepLabel }}
|
||||||
</label>
|
</label>
|
||||||
<!-- Toggle link for entity type selection -->
|
<!-- Toggle link for entity type selection -->
|
||||||
<div class="text-right mb-4">
|
<div class="text-center mb-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="text-sm text-blue-600 hover:text-blue-800 underline font-medium"
|
class="text-xs text-blue-600 hover:underline uppercase"
|
||||||
@click="handleToggleEntityType"
|
@click="handleToggleEntityType"
|
||||||
>
|
>
|
||||||
{{ toggleLinkText }}
|
{{ toggleLinkText }}
|
||||||
|
|||||||
@@ -129,7 +129,7 @@
|
|||||||
@click="disabled ? notifyLocked() : addGroup()"
|
@click="disabled ? notifyLocked() : addGroup()"
|
||||||
>
|
>
|
||||||
<font-awesome icon="plus" class="text-sm" />
|
<font-awesome icon="plus" class="text-sm" />
|
||||||
New Group
|
New Do-Not-Pair Group
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1175,11 +1175,6 @@ export const NOTIFY_GIFTED_DETAILS_DELETE_IMAGE_CONFIRM = {
|
|||||||
message: "",
|
message: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NOTIFY_GIFTED_DETAILS_DELETE_IMAGE_ERROR = {
|
|
||||||
title: "Error",
|
|
||||||
message: "There was a problem deleting the image.",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const NOTIFY_GIFTED_DETAILS_NO_IDENTIFIER = {
|
export const NOTIFY_GIFTED_DETAILS_NO_IDENTIFIER = {
|
||||||
title: "Missing Identifier",
|
title: "Missing Identifier",
|
||||||
message: "You must select an identifier before you can record a give.",
|
message: "You must select an identifier before you can record a give.",
|
||||||
|
|||||||
@@ -169,12 +169,61 @@ async function checkAndStoreNativeSharedImage(): Promise<{
|
|||||||
// Check if we have valid image data (base64 must be non-null and non-empty)
|
// Check if we have valid image data (base64 must be non-null and non-empty)
|
||||||
if (result && result.base64 && result.base64.trim().length > 0) {
|
if (result && result.base64 && result.base64.trim().length > 0) {
|
||||||
const fileName = result.fileName || "shared-image.jpg";
|
const fileName = result.fileName || "shared-image.jpg";
|
||||||
|
const shareId = result.shareId;
|
||||||
|
const shareIdLog = shareId || "unknown";
|
||||||
|
|
||||||
// Store in temp database using extracted method
|
// Hand off to the app: store in temp database using extracted method
|
||||||
logger.info(
|
logger.info(
|
||||||
"[Main] Native shared image found (via plugin), storing in temp DB",
|
`[Main] Shared image processing started shareId=${shareIdLog} fileName=${fileName}`,
|
||||||
);
|
);
|
||||||
await storeSharedImageInTempDB(result.base64, fileName);
|
|
||||||
|
try {
|
||||||
|
await storeSharedImageInTempDB(result.base64, fileName);
|
||||||
|
} catch (storeError) {
|
||||||
|
// Processing failed - leave the shared item intact (do NOT mark processed)
|
||||||
|
logger.error(
|
||||||
|
`[Main] Shared image processing failed shareId=${shareIdLog}:`,
|
||||||
|
storeError,
|
||||||
|
);
|
||||||
|
isProcessingSharedImage = false;
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`[Main] Shared image processing succeeded shareId=${shareIdLog} fileName=${fileName}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Signal explicit completion only after successful processing.
|
||||||
|
// iOS-only: the markProcessed API is not implemented on Android.
|
||||||
|
if (shareId && Capacitor.getPlatform() === "ios") {
|
||||||
|
let marked = false;
|
||||||
|
try {
|
||||||
|
logger.info(`[Main] markProcessed invoked shareId=${shareId}`);
|
||||||
|
await SharedImage.markProcessed({ shareId });
|
||||||
|
marked = true;
|
||||||
|
} catch (markError) {
|
||||||
|
// A failure to mark processed must not fail the (already successful)
|
||||||
|
// share processing; the share simply remains available for retry.
|
||||||
|
logger.warn(
|
||||||
|
`[Main] markProcessed failed shareId=${shareId}:`,
|
||||||
|
markError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 1F: native cleanup ONLY after both processing and markProcessed
|
||||||
|
// succeed. A cleanup failure is logged but does not fail the import,
|
||||||
|
// since the image has already been handed off to the app.
|
||||||
|
if (marked) {
|
||||||
|
try {
|
||||||
|
await SharedImage.clearProcessedShare();
|
||||||
|
} catch (cleanupError) {
|
||||||
|
logger.warn(
|
||||||
|
`[Main] cleanup failed shareId=${shareId}:`,
|
||||||
|
cleanupError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
isProcessingSharedImage = false;
|
isProcessingSharedImage = false;
|
||||||
return { success: true, fileName };
|
return { success: true, fileName };
|
||||||
@@ -405,22 +454,22 @@ logger.info(`[Main] ✅ App mounted successfully`);
|
|||||||
// Check for shared image on initial load (in case app was launched from share sheet)
|
// Check for shared image on initial load (in case app was launched from share sheet)
|
||||||
// On Android, share intents are processed in MainActivity.onCreate, so we need to check
|
// On Android, share intents are processed in MainActivity.onCreate, so we need to check
|
||||||
// after a delay to ensure the native code has finished processing
|
// after a delay to ensure the native code has finished processing
|
||||||
if (
|
if (Capacitor.isNativePlatform() && Capacitor.getPlatform() === "android") {
|
||||||
Capacitor.isNativePlatform() &&
|
// Android behavior unchanged: multiple checks with increasing delays because
|
||||||
(Capacitor.getPlatform() === "ios" || Capacitor.getPlatform() === "android")
|
// share intent processing happens in onCreate, which may complete after JS loads.
|
||||||
) {
|
const checkDelays = [500, 1500, 3000]; // Android needs more time for share intent processing
|
||||||
// Use multiple checks with increasing delays to handle timing issues
|
|
||||||
// Android share intent processing happens in onCreate, which may complete after JS loads
|
|
||||||
const checkDelays =
|
|
||||||
Capacitor.getPlatform() === "android"
|
|
||||||
? [500, 1500, 3000] // Android needs more time for share intent processing
|
|
||||||
: [1000]; // iOS is faster
|
|
||||||
|
|
||||||
checkDelays.forEach((delay) => {
|
checkDelays.forEach((delay) => {
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
await checkForSharedImageAndNavigate();
|
await checkForSharedImageAndNavigate();
|
||||||
}, delay);
|
}, delay);
|
||||||
});
|
});
|
||||||
|
} else if (Capacitor.isNativePlatform() && Capacitor.getPlatform() === "ios") {
|
||||||
|
// Phase 2B-3: the SharedImage plugin is now registered deterministically from
|
||||||
|
// AppBridgeViewController.capacitorDidLoad() before the web layer loads, so it
|
||||||
|
// is guaranteed to exist here. Perform the initial shared-image check
|
||||||
|
// immediately without waiting/polling for plugin readiness.
|
||||||
|
void checkForSharedImageAndNavigate();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for app state changes to detect when app becomes active
|
// Listen for app state changes to detect when app becomes active
|
||||||
|
|||||||
@@ -18,4 +18,16 @@ export class SharedImagePluginWeb
|
|||||||
async hasSharedImage(): Promise<{ hasImage: boolean }> {
|
async hasSharedImage(): Promise<{ hasImage: boolean }> {
|
||||||
return { hasImage: false };
|
return { hasImage: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async markProcessed(_options: {
|
||||||
|
shareId: string;
|
||||||
|
}): Promise<{ success: boolean }> {
|
||||||
|
// Web platform doesn't support native sharing - no-op
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearProcessedShare(): Promise<{ success: boolean }> {
|
||||||
|
// Web platform doesn't support native sharing - no-op
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,18 @@
|
|||||||
export interface SharedImageResult {
|
export interface SharedImageResult {
|
||||||
base64: string;
|
base64: string;
|
||||||
fileName: string;
|
fileName: string;
|
||||||
|
/**
|
||||||
|
* Identifier of the current share (iOS `sharedPhotoShareId`).
|
||||||
|
* Pass this to `markProcessed()` to signal explicit completion.
|
||||||
|
*/
|
||||||
|
shareId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SharedImagePlugin {
|
export interface SharedImagePlugin {
|
||||||
/**
|
/**
|
||||||
* Get shared image data from native layer
|
* Get shared image data from native layer
|
||||||
* Returns base64 string and fileName, or null if no image exists
|
* Returns base64 string and fileName, or null if no image exists
|
||||||
* Clears the data after reading to prevent re-reading
|
* Read-only on iOS: native metadata and file are left intact after retrieval (Phase 1C)
|
||||||
*/
|
*/
|
||||||
getSharedImage(): Promise<SharedImageResult | null>;
|
getSharedImage(): Promise<SharedImageResult | null>;
|
||||||
|
|
||||||
@@ -20,4 +25,29 @@ export interface SharedImagePlugin {
|
|||||||
* Useful for quick checks before calling getSharedImage()
|
* Useful for quick checks before calling getSharedImage()
|
||||||
*/
|
*/
|
||||||
hasSharedImage(): Promise<{ hasImage: boolean }>;
|
hasSharedImage(): Promise<{ hasImage: boolean }>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark a share as explicitly processed by the application layer (Phase 1D, iOS only).
|
||||||
|
*
|
||||||
|
* Records a completion signal for the given shareId on the native side.
|
||||||
|
* This does NOT delete the stored image file or clear share metadata;
|
||||||
|
* file cleanup is deferred to a later phase.
|
||||||
|
*
|
||||||
|
* On web this is a no-op that resolves with success=false.
|
||||||
|
*
|
||||||
|
* @param options.shareId The share identifier reported by getSharedImage()
|
||||||
|
*/
|
||||||
|
markProcessed(options: { shareId: string }): Promise<{ success: boolean }>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Permanently remove the currently processed share from native storage
|
||||||
|
* (Phase 1F, iOS only).
|
||||||
|
*
|
||||||
|
* Deletes the shared image file and all share metadata. Idempotent: missing
|
||||||
|
* files/keys resolve successfully. Should only be called after the share has
|
||||||
|
* been successfully processed AND marked processed.
|
||||||
|
*
|
||||||
|
* On web this is a no-op that resolves with success=false.
|
||||||
|
*/
|
||||||
|
clearProcessedShare(): Promise<{ success: boolean }>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,6 +136,11 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: "help-onboarding",
|
name: "help-onboarding",
|
||||||
component: () => import("../views/HelpOnboardingView.vue"),
|
component: () => import("../views/HelpOnboardingView.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/help-terms",
|
||||||
|
name: "help-terms",
|
||||||
|
component: () => import("../views/HelpTermsView.vue"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
name: "home",
|
name: "home",
|
||||||
|
|||||||
@@ -123,10 +123,10 @@ export class CapacitorQRScanner implements QRScannerService {
|
|||||||
|
|
||||||
// Add listener for barcode scans
|
// Add listener for barcode scans
|
||||||
const handle = await BarcodeScanner.addListener(
|
const handle = await BarcodeScanner.addListener(
|
||||||
"barcodeScanned",
|
"barcodesScanned",
|
||||||
(result) => {
|
(result) => {
|
||||||
if (this.scanListener && result.barcode?.rawValue) {
|
if (this.scanListener && result.barcodes?.[0]?.rawValue) {
|
||||||
this.scanListener.onScan(result.barcode.rawValue);
|
this.scanListener.onScan(result.barcodes[0].rawValue);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -62,10 +62,6 @@ export class NativeNotificationService implements NotificationServiceInterface {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Request notification permissions from the OS
|
|
||||||
* Shows native permission dialog on first call
|
|
||||||
*/
|
|
||||||
/**
|
/**
|
||||||
* Request notification permissions from the OS
|
* Request notification permissions from the OS
|
||||||
* Shows native permission dialog on first call
|
* Shows native permission dialog on first call
|
||||||
|
|||||||
@@ -1402,54 +1402,6 @@ export default class AccountViewView extends Vue {
|
|||||||
}, 150);
|
}, 150);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle dev-only 10-minute rollover for daily reminder. Saves the setting and,
|
|
||||||
* if reminder is already on, reschedules so the plugin uses the new interval.
|
|
||||||
*/
|
|
||||||
async toggleReminderFastRollover(): Promise<void> {
|
|
||||||
const next = !this.reminderFastRolloverForTesting;
|
|
||||||
await this.$saveSettings({
|
|
||||||
reminderFastRolloverForTesting: next,
|
|
||||||
});
|
|
||||||
this.reminderFastRolloverForTesting = next;
|
|
||||||
|
|
||||||
if (this.notifyingReminder) {
|
|
||||||
try {
|
|
||||||
const service = NotificationService.getInstance();
|
|
||||||
if (Capacitor.getPlatform() !== "android") {
|
|
||||||
await service.cancelDailyNotification();
|
|
||||||
}
|
|
||||||
const time24h = this.parseTimeTo24Hour(this.notifyingReminderTime);
|
|
||||||
const title = "Daily Reminder";
|
|
||||||
const body =
|
|
||||||
this.notifyingReminderMessage ||
|
|
||||||
"Click to share some gratitude with the world -- even if they're unnamed.";
|
|
||||||
await service.scheduleDailyNotification({
|
|
||||||
time: time24h,
|
|
||||||
title,
|
|
||||||
body,
|
|
||||||
priority: "normal",
|
|
||||||
...(next ? { rolloverIntervalMinutes: 10 } : {}),
|
|
||||||
});
|
|
||||||
this.notify.success(
|
|
||||||
next
|
|
||||||
? "Reminder will repeat every 10 minutes (testing)."
|
|
||||||
: "Reminder will repeat daily (24h).",
|
|
||||||
TIMEOUTS.STANDARD,
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(
|
|
||||||
"[AccountViewView] Reschedule after fast-rollover toggle failed:",
|
|
||||||
err,
|
|
||||||
);
|
|
||||||
this.notify.error(
|
|
||||||
"Failed to update reminder interval. Please try again.",
|
|
||||||
TIMEOUTS.STANDARD,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse time string (e.g., "5:22 PM") to 24-hour format (e.g., "17:22")
|
* Parse time string (e.g., "5:22 PM") to 24-hour format (e.g., "17:22")
|
||||||
*/
|
*/
|
||||||
@@ -2116,6 +2068,10 @@ export default class AccountViewView extends Vue {
|
|||||||
hasLocation: result.includeLocation,
|
hasLocation: result.includeLocation,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (response.data.userMessage) {
|
||||||
|
this.notify.info(response.data.userMessage);
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} else {
|
} else {
|
||||||
logger.debug("[AccountViewView] No profile data found in response:", {
|
logger.debug("[AccountViewView] No profile data found in response:", {
|
||||||
@@ -2123,6 +2079,10 @@ export default class AccountViewView extends Vue {
|
|||||||
hasData: !!response.data,
|
hasData: !!response.data,
|
||||||
hasDataData: !!(response.data && response.data.data),
|
hasDataData: !!(response.data && response.data.data),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (response.data?.userMessage) {
|
||||||
|
this.notify.info(response.data.userMessage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -2229,6 +2189,10 @@ export default class AccountViewView extends Vue {
|
|||||||
status: response.status,
|
status: response.status,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (response.data?.userMessage) {
|
||||||
|
this.notify.info(response.data.userMessage);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
// Handle specific HTTP status codes cleanly to suppress console spam
|
// Handle specific HTTP status codes cleanly to suppress console spam
|
||||||
@@ -2345,6 +2309,10 @@ export default class AccountViewView extends Vue {
|
|||||||
status: response.status,
|
status: response.status,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (response.data?.userMessage) {
|
||||||
|
this.notify.info(response.data.userMessage);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
// Handle specific HTTP status codes cleanly to suppress console spam
|
// Handle specific HTTP status codes cleanly to suppress console spam
|
||||||
|
|||||||
@@ -118,11 +118,13 @@ import {
|
|||||||
CONTACT_CSV_HEADER,
|
CONTACT_CSV_HEADER,
|
||||||
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
|
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
|
||||||
generateEndorserJwtUrlForAccount,
|
generateEndorserJwtUrlForAccount,
|
||||||
|
register,
|
||||||
setVisibilityUtil,
|
setVisibilityUtil,
|
||||||
} from "../libs/endorserServer";
|
} from "../libs/endorserServer";
|
||||||
import UserNameDialog from "../components/UserNameDialog.vue";
|
import UserNameDialog from "../components/UserNameDialog.vue";
|
||||||
import { retrieveAccountMetadata } from "../libs/util";
|
import { retrieveAccountMetadata } from "../libs/util";
|
||||||
|
|
||||||
|
import { AxiosError } from "axios";
|
||||||
import { Account } from "@/db/tables/accounts";
|
import { Account } from "@/db/tables/accounts";
|
||||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||||
import {
|
import {
|
||||||
@@ -139,7 +141,10 @@ import {
|
|||||||
NOTIFY_QR_URL_COPIED,
|
NOTIFY_QR_URL_COPIED,
|
||||||
NOTIFY_QR_CODE_HELP,
|
NOTIFY_QR_CODE_HELP,
|
||||||
NOTIFY_QR_DID_COPIED,
|
NOTIFY_QR_DID_COPIED,
|
||||||
|
NOTIFY_QR_REGISTRATION_SUBMITTED,
|
||||||
|
NOTIFY_QR_REGISTRATION_ERROR,
|
||||||
createQRContactAddedMessage,
|
createQRContactAddedMessage,
|
||||||
|
createQRRegistrationSuccessMessage,
|
||||||
QR_TIMEOUT_MEDIUM,
|
QR_TIMEOUT_MEDIUM,
|
||||||
QR_TIMEOUT_STANDARD,
|
QR_TIMEOUT_STANDARD,
|
||||||
QR_TIMEOUT_LONG,
|
QR_TIMEOUT_LONG,
|
||||||
@@ -204,6 +209,7 @@ export default class ContactQRScanFull extends Vue {
|
|||||||
activeDid = "";
|
activeDid = "";
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
givenName = "";
|
givenName = "";
|
||||||
|
hideRegisterPromptOnNewContact = false;
|
||||||
isRegistered = false;
|
isRegistered = false;
|
||||||
profileImageUrl = "";
|
profileImageUrl = "";
|
||||||
qrValue = "";
|
qrValue = "";
|
||||||
@@ -278,6 +284,8 @@ export default class ContactQRScanFull extends Vue {
|
|||||||
|
|
||||||
this.apiServer = settings.apiServer || "";
|
this.apiServer = settings.apiServer || "";
|
||||||
this.givenName = settings.firstName || "";
|
this.givenName = settings.firstName || "";
|
||||||
|
this.hideRegisterPromptOnNewContact =
|
||||||
|
!!settings.hideRegisterPromptOnNewContact;
|
||||||
this.isRegistered = !!settings.isRegistered;
|
this.isRegistered = !!settings.isRegistered;
|
||||||
this.profileImageUrl = settings.profileImageUrl || "";
|
this.profileImageUrl = settings.profileImageUrl || "";
|
||||||
|
|
||||||
@@ -575,6 +583,34 @@ export default class ContactQRScanFull extends Vue {
|
|||||||
createQRContactAddedMessage(!!this.activeDid),
|
createQRContactAddedMessage(!!this.activeDid),
|
||||||
QR_TIMEOUT_STANDARD,
|
QR_TIMEOUT_STANDARD,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.isRegistered &&
|
||||||
|
!this.hideRegisterPromptOnNewContact &&
|
||||||
|
!contact.registered
|
||||||
|
) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "modal",
|
||||||
|
type: "confirm",
|
||||||
|
title: "Register",
|
||||||
|
text: "Do you want to register them?",
|
||||||
|
onCancel: async (stopAsking?: boolean) => {
|
||||||
|
await this.handleRegistrationPromptResponse(stopAsking);
|
||||||
|
},
|
||||||
|
onNo: async (stopAsking?: boolean) => {
|
||||||
|
await this.handleRegistrationPromptResponse(stopAsking);
|
||||||
|
},
|
||||||
|
onYes: async () => {
|
||||||
|
await this.register(contact);
|
||||||
|
},
|
||||||
|
promptToStopAsking: true,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error saving contact to database:", {
|
logger.error("Error saving contact to database:", {
|
||||||
did: contact.did,
|
did: contact.did,
|
||||||
@@ -585,6 +621,74 @@ export default class ContactQRScanFull extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async register(contact: Contact) {
|
||||||
|
logger.debug("Submitting contact registration", {
|
||||||
|
did: contact.did,
|
||||||
|
name: contact.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const regResult = await register(
|
||||||
|
this.activeDid,
|
||||||
|
this.apiServer,
|
||||||
|
this.axios,
|
||||||
|
contact,
|
||||||
|
);
|
||||||
|
this.notify.toast("Submitted", NOTIFY_QR_REGISTRATION_SUBMITTED.message);
|
||||||
|
if (regResult.success) {
|
||||||
|
contact.registered = true;
|
||||||
|
await this.$updateContact(contact.did, { registered: true });
|
||||||
|
logger.debug("Contact registration successful", { did: contact.did });
|
||||||
|
|
||||||
|
this.notify.success(
|
||||||
|
createQRRegistrationSuccessMessage(contact.name || ""),
|
||||||
|
QR_TIMEOUT_LONG,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.notify.error(
|
||||||
|
(regResult.error as string) || NOTIFY_QR_REGISTRATION_ERROR.message,
|
||||||
|
QR_TIMEOUT_LONG,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error registering contact:", {
|
||||||
|
did: contact.did,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
|
});
|
||||||
|
let userMessage = "There was an error.";
|
||||||
|
const serverError = error as AxiosError;
|
||||||
|
if (serverError) {
|
||||||
|
if (
|
||||||
|
serverError.response?.data &&
|
||||||
|
typeof serverError.response.data === "object" &&
|
||||||
|
"message" in serverError.response.data
|
||||||
|
) {
|
||||||
|
userMessage = (serverError.response.data as { message: string })
|
||||||
|
.message;
|
||||||
|
} else if (serverError.message) {
|
||||||
|
userMessage = serverError.message;
|
||||||
|
} else {
|
||||||
|
userMessage = JSON.stringify(serverError.toJSON());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
userMessage = error as string;
|
||||||
|
}
|
||||||
|
this.notify.error(userMessage, QR_TIMEOUT_LONG);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleRegistrationPromptResponse(
|
||||||
|
stopAsking?: boolean,
|
||||||
|
): Promise<void> {
|
||||||
|
if (stopAsking) {
|
||||||
|
await this.$saveSettings({
|
||||||
|
hideRegisterPromptOnNewContact: stopAsking,
|
||||||
|
});
|
||||||
|
this.hideRegisterPromptOnNewContact = stopAsking;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vue lifecycle hook - component mounted
|
* Vue lifecycle hook - component mounted
|
||||||
* Sets up event listeners and starts scanning automatically
|
* Sets up event listeners and starts scanning automatically
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<QuickNav />
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
<TopMessage />
|
<TopMessage />
|
||||||
@@ -263,7 +265,6 @@ import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
|
|||||||
import {
|
import {
|
||||||
NOTIFY_GIFTED_DETAILS_RETRIEVAL_ERROR,
|
NOTIFY_GIFTED_DETAILS_RETRIEVAL_ERROR,
|
||||||
NOTIFY_GIFTED_DETAILS_DELETE_IMAGE_CONFIRM,
|
NOTIFY_GIFTED_DETAILS_DELETE_IMAGE_CONFIRM,
|
||||||
NOTIFY_GIFTED_DETAILS_DELETE_IMAGE_ERROR,
|
|
||||||
NOTIFY_GIFTED_DETAILS_NO_IDENTIFIER,
|
NOTIFY_GIFTED_DETAILS_NO_IDENTIFIER,
|
||||||
NOTIFY_GIFT_ERROR_NEGATIVE_AMOUNT,
|
NOTIFY_GIFT_ERROR_NEGATIVE_AMOUNT,
|
||||||
NOTIFY_GIFTED_DETAILS_RECORDING_GIVE,
|
NOTIFY_GIFTED_DETAILS_RECORDING_GIVE,
|
||||||
@@ -302,6 +303,7 @@ export default class GiftedDetails extends Vue {
|
|||||||
giverName = "";
|
giverName = "";
|
||||||
hideBackButton = false;
|
hideBackButton = false;
|
||||||
imageUrl = "";
|
imageUrl = "";
|
||||||
|
imageUrlToDelete = "";
|
||||||
message = "";
|
message = "";
|
||||||
offerId = "";
|
offerId = "";
|
||||||
prevCredToEdit?: GenericCredWrapper<GiveActionClaim>;
|
prevCredToEdit?: GenericCredWrapper<GiveActionClaim>;
|
||||||
@@ -517,7 +519,10 @@ export default class GiftedDetails extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
this.deleteImage(); // not awaiting, so they'll go back immediately
|
// Only delete freshly uploaded images, not ones from an existing claim
|
||||||
|
if (this.imageUrl && this.imageUrl !== this.prevCredToEdit?.claim?.image) {
|
||||||
|
this.deleteImage(this.imageUrl); // not awaiting, so they'll go back immediately
|
||||||
|
}
|
||||||
if (this.destinationPathAfter) {
|
if (this.destinationPathAfter) {
|
||||||
(this.$router as Router).push({ path: this.destinationPathAfter });
|
(this.$router as Router).push({ path: this.destinationPathAfter });
|
||||||
} else {
|
} else {
|
||||||
@@ -526,7 +531,10 @@ export default class GiftedDetails extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cancelBack() {
|
cancelBack() {
|
||||||
this.deleteImage(); // not awaiting, so they'll go back immediately
|
// Only delete freshly uploaded images, not ones from an existing claim
|
||||||
|
if (this.imageUrl && this.imageUrl !== this.prevCredToEdit?.claim?.image) {
|
||||||
|
this.deleteImage(this.imageUrl); // not awaiting, so they'll go back immediately
|
||||||
|
}
|
||||||
(this.$router as Router).back();
|
(this.$router as Router).back();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -539,13 +547,18 @@ export default class GiftedDetails extends Vue {
|
|||||||
confirmDeleteImage() {
|
confirmDeleteImage() {
|
||||||
this.notify.confirm(
|
this.notify.confirm(
|
||||||
NOTIFY_GIFTED_DETAILS_DELETE_IMAGE_CONFIRM.message,
|
NOTIFY_GIFTED_DETAILS_DELETE_IMAGE_CONFIRM.message,
|
||||||
this.deleteImage,
|
() => {
|
||||||
|
// Stage the image for deletion on submit rather than deleting immediately,
|
||||||
|
// so that canceling the edit doesn't destroy the referenced image.
|
||||||
|
this.imageUrlToDelete = this.imageUrl;
|
||||||
|
this.imageUrl = "";
|
||||||
|
},
|
||||||
TIMEOUTS.LONG,
|
TIMEOUTS.LONG,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteImage() {
|
async deleteImage(imageUrl: string) {
|
||||||
if (!this.imageUrl) {
|
if (!imageUrl) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -559,38 +572,21 @@ export default class GiftedDetails extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
const response = await this.axios.delete(
|
const response = await this.axios.delete(
|
||||||
DEFAULT_IMAGE_API_SERVER +
|
DEFAULT_IMAGE_API_SERVER + "/image/" + encodeURIComponent(imageUrl),
|
||||||
"/image/" +
|
|
||||||
encodeURIComponent(this.imageUrl),
|
|
||||||
{ headers },
|
{ headers },
|
||||||
);
|
);
|
||||||
if (response.status === 204) {
|
if (response.status === 204) {
|
||||||
// don't bother with a notification
|
// don't bother with a notification
|
||||||
// (either they'll simply continue or they're canceling and going back)
|
|
||||||
} else {
|
} else {
|
||||||
logger.error("Problem deleting image:", response);
|
logger.error("Problem deleting image:", response);
|
||||||
this.notify.error(
|
|
||||||
NOTIFY_GIFTED_DETAILS_DELETE_IMAGE_ERROR.message,
|
|
||||||
TIMEOUTS.LONG,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.imageUrl = "";
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error deleting image:", error);
|
logger.error("Error deleting image:", error);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
if ((error as any)?.response?.status === 404) {
|
if ((error as any)?.response?.status === 404) {
|
||||||
logger.log("Weird: the image was already deleted.", error);
|
logger.log("Image was already deleted:", error);
|
||||||
|
|
||||||
this.imageUrl = "";
|
|
||||||
|
|
||||||
// it already doesn't exist so we won't say anything to the user
|
|
||||||
} else {
|
} else {
|
||||||
this.notify.error(
|
logger.error("Failed to delete image from server:", error);
|
||||||
NOTIFY_GIFTED_DETAILS_DELETE_IMAGE_ERROR.message,
|
|
||||||
TIMEOUTS.LONG,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -733,6 +729,12 @@ export default class GiftedDetails extends Vue {
|
|||||||
TIMEOUTS.LONG,
|
TIMEOUTS.LONG,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
// Delete the old image from storage now that the edit is saved
|
||||||
|
if (this.imageUrlToDelete) {
|
||||||
|
this.deleteImage(this.imageUrlToDelete); // not awaiting
|
||||||
|
this.imageUrlToDelete = "";
|
||||||
|
}
|
||||||
|
|
||||||
this.notify.success(
|
this.notify.success(
|
||||||
NOTIFY_GIFTED_DETAILS_GIFT_RECORDED.message,
|
NOTIFY_GIFTED_DETAILS_GIFT_RECORDED.message,
|
||||||
TIMEOUTS.SHORT,
|
TIMEOUTS.SHORT,
|
||||||
|
|||||||
144
src/views/HelpTermsView.vue
Normal file
144
src/views/HelpTermsView.vue
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
<template>
|
||||||
|
<QuickNav />
|
||||||
|
|
||||||
|
<!-- CONTENT -->
|
||||||
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
|
<!-- Sub View Heading -->
|
||||||
|
<div id="SubViewHeading" class="flex gap-4 items-start mb-8">
|
||||||
|
<h1 class="grow text-xl text-center font-semibold leading-tight">
|
||||||
|
Terms & Conditions and Privacy Policies
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<!-- Back -->
|
||||||
|
<a
|
||||||
|
class="order-first text-lg text-center leading-none p-1"
|
||||||
|
@click="$router.go(-1)"
|
||||||
|
>
|
||||||
|
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Help button -->
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'help' }"
|
||||||
|
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
|
||||||
|
>
|
||||||
|
<font-awesome icon="question" class="block text-center w-[1em]" />
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- eslint-disable prettier/prettier -->
|
||||||
|
<div>
|
||||||
|
<p style="display:inline; align-items: center">
|
||||||
|
This work is public domain. (If you like rules, reference
|
||||||
|
<a
|
||||||
|
href="http://creativecommons.org/publicdomain/zero/1.0?ref=chooser-v1"
|
||||||
|
target="_blank"
|
||||||
|
rel="license noopener noreferrer"
|
||||||
|
>
|
||||||
|
<span class="text-blue-500 mr-1">CC0 1.0</span>
|
||||||
|
<img
|
||||||
|
src="../assets/help/creative-commons-circle.svg"
|
||||||
|
alt="CC circle"
|
||||||
|
width="20"
|
||||||
|
class="display: inline"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src="../assets/help/creative-commons-zero.svg"
|
||||||
|
alt="CC zero"
|
||||||
|
width="20"
|
||||||
|
style="display: inline"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
</p>
|
||||||
|
<p class="mt-4">
|
||||||
|
This is offered freely, with the hope that it helps but without any
|
||||||
|
warranty or guarantee. When you share data or even look at information here,
|
||||||
|
you accept the risk that goes with those activities. In other words,
|
||||||
|
if you expect some functionality or you expect some protection, and you
|
||||||
|
feel it is appropriate to force those expectations on the system or its
|
||||||
|
operators or creators, then you are not allowed to use it.
|
||||||
|
</p>
|
||||||
|
<p class="mt-4">
|
||||||
|
Here is how your data is used:
|
||||||
|
</p>
|
||||||
|
<ul class="list-disc list-outside ml-4">
|
||||||
|
<li>
|
||||||
|
If sending images, a server stores them. They can be removed by editing
|
||||||
|
each claim and deleting the image.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
If sending other partner system data (eg. to Trustroots) a public key
|
||||||
|
and message data are stored on a server. Those can be removed via
|
||||||
|
direct personal request (email
|
||||||
|
<a :href="`mailto:${SUPPORT_EMAIL}`" class="text-blue-500">
|
||||||
|
{{ SUPPORT_EMAIL }}
|
||||||
|
</a>).
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
For all other claim data,
|
||||||
|
<a
|
||||||
|
href="https://endorser.ch/privacy-policy"
|
||||||
|
target="_blank"
|
||||||
|
class="text-blue-500"
|
||||||
|
>
|
||||||
|
the Endorser Service has this Privacy Policy.
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<!--
|
||||||
|
This section is for Twilio's A2P Campaign requirements.
|
||||||
|
They say: Ensure it includes the program name, description, message/data rates, message frequency, support contact info, and opt-out instructions (HELP and STOP in bold).
|
||||||
|
They link here for a sample: https://help.twilio.com/articles/223134847-Industry-standards-for-US-Short-Code-Terms-of-Service
|
||||||
|
-->
|
||||||
|
Here are the details for SMS notifications:
|
||||||
|
<ul class="list-disc list-outside ml-4">
|
||||||
|
<li>You may opt to receive SMS messages for two purposes:
|
||||||
|
<ul class="list-disc list-outside ml-4">
|
||||||
|
<li>A daily reminder message</li>
|
||||||
|
<li>A notification of new activity for items that you are watching</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Before enabling these notifications, you must register your phone number and give permission to use it for searches.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Once your phone number is registered and linked to your DID, you can enable or disable either kind of SMS message.
|
||||||
|
You can disable these any time with the same toggle.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
If you lose your credentials, you can register your phone with a different DID.
|
||||||
|
Then you can enable and disable notifications for your phone.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Carriers are not liable for delayed or undelivered messages.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
As always, message and data rates may apply for any messages sent to you from us and to us from you.
|
||||||
|
You will receive at most one of each kind of message per day.
|
||||||
|
If you have any questions about your text plan or data plan, it is best to contact your wireless provider.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Our servers will only store your phone number and the type of notifications you have enabled,
|
||||||
|
along with the explicit signed permission to use it for searches.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- eslint-enable -->
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
|
||||||
|
import QuickNav from "../components/QuickNav.vue";
|
||||||
|
import { SUPPORT_EMAIL } from "../constants/app";
|
||||||
|
|
||||||
|
@Component({ components: { QuickNav } })
|
||||||
|
export default class HelpTermsView extends Vue {
|
||||||
|
SUPPORT_EMAIL = SUPPORT_EMAIL;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -480,46 +480,14 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">What are the terms & conditions and the privacy policy?</h2>
|
<h2 class="text-xl font-semibold">What are the terms & conditions and the privacy policy?</h2>
|
||||||
<p style="display:inline; align-items: center">
|
<p>
|
||||||
This work is public domain. (If you like rules, reference
|
<router-link
|
||||||
<a href="http://creativecommons.org/publicdomain/zero/1.0?ref=chooser-v1" target="_blank" rel="license noopener noreferrer">
|
class="text-blue-500"
|
||||||
<span class="text-blue-500 mr-1">CC0 1.0</span>
|
:to="{ name: 'help-terms' }"
|
||||||
<img
|
>
|
||||||
src="../assets/help/creative-commons-circle.svg"
|
Read them here.
|
||||||
alt="CC circle"
|
</router-link>
|
||||||
width="20"
|
|
||||||
class="display: inline"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
src="../assets/help/creative-commons-zero.svg"
|
|
||||||
alt="CC zero"
|
|
||||||
width="20"
|
|
||||||
style="display: inline"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
.) This is offered freely, with the hope that it helps but without any warranty or guarantee;
|
|
||||||
if it helps you then enjoy using it,
|
|
||||||
but if you may try to forcibly collect damages for things you think it should do (or not do)
|
|
||||||
then don't use it.
|
|
||||||
<br />
|
|
||||||
As for data & privacy:
|
|
||||||
</p>
|
</p>
|
||||||
<ul class="list-disc list-outside ml-4">
|
|
||||||
<li>
|
|
||||||
If sending images, a server stores them. They can be removed by editing each claim
|
|
||||||
and deleting the image.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
If sending other partner system data (eg. to Trustroots) a public key and message
|
|
||||||
data are stored on a server. Those can be removed via direct personal request (via contact below).
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
For all other claim data,
|
|
||||||
<a href="https://endorser.ch/privacy-policy" target="_blank" class="text-blue-500">
|
|
||||||
the Endorser Service has this Privacy Policy.
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">How can I contribute?</h2>
|
<h2 class="text-xl font-semibold">How can I contribute?</h2>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -109,12 +109,14 @@ Raymer * @version 1.0.0 */
|
|||||||
|
|
||||||
<div v-if="isUserRegistered" id="sectionRecordSomethingGiven">
|
<div v-if="isUserRegistered" id="sectionRecordSomethingGiven">
|
||||||
<!-- Record Quick-Action -->
|
<!-- Record Quick-Action -->
|
||||||
<div class="mb-6">
|
<div
|
||||||
<div class="flex gap-2 items-center mb-2">
|
class="bg-slate-100 border border-slate-300 rounded-md px-4 py-2.5 mb-4"
|
||||||
|
>
|
||||||
|
<div class="flex gap-2 justify-between items-center ps-8">
|
||||||
<!-- Thank button - always visible and unchanged -->
|
<!-- Thank button - always visible and unchanged -->
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="text-center text-base uppercase bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white flex items-center justify-center gap-2 px-4 py-3 rounded-full"
|
class="text-center text-xl uppercase font-bold bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white flex items-center justify-center gap-2 mx-auto px-6 py-2 rounded-lg"
|
||||||
@click="openPersonDialog()"
|
@click="openPersonDialog()"
|
||||||
>
|
>
|
||||||
<font-awesome icon="plus" />
|
<font-awesome icon="plus" />
|
||||||
@@ -122,25 +124,29 @@ Raymer * @version 1.0.0 */
|
|||||||
</button>
|
</button>
|
||||||
<!-- Plus button - appears when scrolled, positioned over house-chimney icon -->
|
<!-- Plus button - appears when scrolled, positioned over house-chimney icon -->
|
||||||
<transition
|
<transition
|
||||||
enter-active-class="transition-all duration-1000 ease-out"
|
enter-active-class="transition-all duration-500 ease-out"
|
||||||
leave-active-class="transition-all duration-1000 ease-in"
|
leave-active-class="transition-all duration-500 ease-in"
|
||||||
enter-from-class="scale-0"
|
enter-from-class="opacity-0"
|
||||||
enter-to-class="scale-100"
|
enter-to-class="opacity-100"
|
||||||
leave-from-class="scale-100"
|
leave-from-class="opacity-100"
|
||||||
leave-to-class="scale-0"
|
leave-to-class="opacity-0"
|
||||||
>
|
>
|
||||||
<button
|
<div
|
||||||
v-if="isScrolled"
|
v-if="isScrolled"
|
||||||
type="button"
|
class="bg-gradient-to-t from-white to-transparent fixed inset-x-0 bottom-[calc(4.75rem+max(env(safe-area-inset-bottom),var(--safe-area-inset-bottom,0px)))] px-4 pb-3 w-full z-[49]"
|
||||||
class="fixed bottom-10 p-4 w-14 h-14 z-50 text-center bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white rounded-full flex items-center justify-center"
|
|
||||||
:style="getButtonPosition()"
|
|
||||||
@click="openPersonDialog()"
|
|
||||||
>
|
>
|
||||||
<font-awesome icon="plus" />
|
<button
|
||||||
</button>
|
type="button"
|
||||||
|
class="text-center text-xl uppercase font-bold bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white flex items-center justify-center gap-2 mx-auto px-6 py-2 rounded-lg drop-shadow-[0_0_10px_rgba(255,255,255,1)]"
|
||||||
|
@click="openPersonDialog()"
|
||||||
|
>
|
||||||
|
<font-awesome icon="plus" />
|
||||||
|
<span>Thank</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
<button
|
<button
|
||||||
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
|
class="block text-sm text-center text-white bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
|
||||||
@click="openGiftedPrompts()"
|
@click="openGiftedPrompts()"
|
||||||
>
|
>
|
||||||
<font-awesome
|
<font-awesome
|
||||||
@@ -559,7 +565,7 @@ export default class HomeView extends Vue {
|
|||||||
const scrollTop = appElement
|
const scrollTop = appElement
|
||||||
? appElement.scrollTop
|
? appElement.scrollTop
|
||||||
: window.pageYOffset || document.documentElement.scrollTop || 0;
|
: window.pageYOffset || document.documentElement.scrollTop || 0;
|
||||||
const shouldBeScrolled = scrollTop > 100;
|
const shouldBeScrolled = scrollTop > 120;
|
||||||
if (this.isScrolled !== shouldBeScrolled) {
|
if (this.isScrolled !== shouldBeScrolled) {
|
||||||
this.isScrolled = shouldBeScrolled;
|
this.isScrolled = shouldBeScrolled;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -366,7 +366,8 @@
|
|||||||
Do Not Pair Together
|
Do Not Pair Together
|
||||||
</h4>
|
</h4>
|
||||||
<p class="text-xs text-gray-500 mb-2">
|
<p class="text-xs text-gray-500 mb-2">
|
||||||
People in the same group will not be matched with each other.
|
People in the same do-not-pair group will not be matched with each
|
||||||
|
other.
|
||||||
</p>
|
</p>
|
||||||
<p v-if="hasActiveMatches" class="text-xs text-amber-600 mb-2">
|
<p v-if="hasActiveMatches" class="text-xs text-amber-600 mb-2">
|
||||||
Erase matches to change restrictions.
|
Erase matches to change restrictions.
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ export default class QuickActionBvcBeginView extends Vue {
|
|||||||
);
|
);
|
||||||
this.notify.error(
|
this.notify.error(
|
||||||
timeResult?.error || NOTIFY_BVC_TIME_ERROR.message,
|
timeResult?.error || NOTIFY_BVC_TIME_ERROR.message,
|
||||||
TIMEOUTS.LONG,
|
timeResult?.error ? TIMEOUTS.MODAL : TIMEOUTS.LONG,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -251,7 +251,7 @@ export default class QuickActionBvcBeginView extends Vue {
|
|||||||
);
|
);
|
||||||
this.notify.error(
|
this.notify.error(
|
||||||
attendResult?.error || NOTIFY_BVC_ATTENDANCE_ERROR.message,
|
attendResult?.error || NOTIFY_BVC_ATTENDANCE_ERROR.message,
|
||||||
TIMEOUTS.LONG,
|
attendResult?.error ? TIMEOUTS.MODAL : TIMEOUTS.LONG,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -276,7 +276,7 @@ export default class QuickActionBvcBeginView extends Vue {
|
|||||||
logger.error("[QuickActionBvcBeginView] Error sending claims:", error);
|
logger.error("[QuickActionBvcBeginView] Error sending claims:", error);
|
||||||
this.notify.error(
|
this.notify.error(
|
||||||
error.userMessage || NOTIFY_BVC_SUBMISSION_ERROR.message,
|
error.userMessage || NOTIFY_BVC_SUBMISSION_ERROR.message,
|
||||||
TIMEOUTS.LONG,
|
error.userMessage ? TIMEOUTS.MODAL : TIMEOUTS.LONG,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,11 +130,13 @@ test('Add contact, record gift, confirm gift', async ({ page }) => {
|
|||||||
|
|
||||||
// Refresh home view and check gift
|
// Refresh home view and check gift
|
||||||
await page.goto('./');
|
await page.goto('./');
|
||||||
|
// Wait for async data (e.g. "new offers" banner) to finish loading so the layout
|
||||||
|
// is stable before clicking — otherwise a layout shift can redirect the click.
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
// Firefox complains on load the initial feed here when we use the test server.
|
const item = page.locator('li').filter({ hasText: finalTitle }).locator('[data-testid="circle-info-link"]');
|
||||||
// It may be similar to the CORS problem below.
|
await expect(item).toBeVisible();
|
||||||
const item = await page.locator('li:first-child').filter({ hasText: finalTitle });
|
await item.click();
|
||||||
await item.locator('[data-testid="circle-info-link"]').click();
|
|
||||||
await expect(page.getByRole('heading', { name: 'Verifiable Claim Details' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Verifiable Claim Details' })).toBeVisible();
|
||||||
await expect(page.getByText(finalTitle, { exact: true })).toBeVisible();
|
await expect(page.getByText(finalTitle, { exact: true })).toBeVisible();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user