Compare commits
11 Commits
accountvie
...
web-share-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09a230f43e | ||
|
|
eff4126043 | ||
|
|
ae49c0e907 | ||
|
|
1b4ab7a500 | ||
| 6ec2002cb0 | |||
| 7d295dd062 | |||
| 5f1b4dcc21 | |||
| 11f122552d | |||
| c84a3b6705 | |||
| e64902321f | |||
|
|
c4eb6f2d1d |
10
BUILDING.md
10
BUILDING.md
@@ -1161,7 +1161,7 @@ export GEM_PATH=$shortened_path
|
|||||||
##### 1. Bump the version in package.json for `MARKETING_VERSION`, then `grep CURRENT_PROJECT_VERSION ios/App/App.xcodeproj/project.pbxproj` and add 1 for the numbered version;
|
##### 1. Bump the version in package.json for `MARKETING_VERSION`, then `grep CURRENT_PROJECT_VERSION ios/App/App.xcodeproj/project.pbxproj` and add 1 for the numbered version;
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ios/App && xcrun agvtool new-version 47 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.1.2;/g" App.xcodeproj/project.pbxproj && cd -
|
cd ios/App && xcrun agvtool new-version 48 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.1.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
|
||||||
```
|
```
|
||||||
@@ -1304,8 +1304,8 @@ The recommended way to build for Android is using the automated build script:
|
|||||||
# Standard build and open Android Studio
|
# Standard build and open Android Studio
|
||||||
./scripts/build-android.sh
|
./scripts/build-android.sh
|
||||||
|
|
||||||
# Build with specific version numbers
|
# Build with specific version numbers -- doesn't change source files
|
||||||
./scripts/build-android.sh --version 1.0.3 --build-number 35
|
#./scripts/build-android.sh --version 1.1.3 --build-number 48
|
||||||
|
|
||||||
# Build without opening Android Studio (for CI/CD)
|
# Build without opening Android Studio (for CI/CD)
|
||||||
./scripts/build-android.sh --no-studio
|
./scripts/build-android.sh --no-studio
|
||||||
@@ -1319,8 +1319,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 47/g' android/app/build.gradle
|
perl -p -i -e 's/versionCode .*/versionCode 48/g' android/app/build.gradle
|
||||||
perl -p -i -e 's/versionName .*/versionName "1.1.2"/g' android/app/build.gradle
|
perl -p -i -e 's/versionName .*/versionName "1.1.3"/g' android/app/build.gradle
|
||||||
```
|
```
|
||||||
|
|
||||||
##### 2. Build
|
##### 2. Build
|
||||||
|
|||||||
11
CHANGELOG.md
11
CHANGELOG.md
@@ -6,8 +6,17 @@ 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.1.2] - 2025.11.06
|
## [1.1.3] - 2025.11.19
|
||||||
|
### Changed
|
||||||
|
- Project selection in dialogs now reaches out to server when filtering
|
||||||
|
- Project selection during onboarding meeting is a search (not an input box)
|
||||||
|
- Improve the switching of agent when agent edits a project
|
||||||
|
### Fixed
|
||||||
|
- Reassignment of "you" as recipient when changing giver project
|
||||||
|
- Bad counts for project-change notification on front page
|
||||||
|
|
||||||
|
|
||||||
|
## [1.1.2] - 2025.11.06
|
||||||
### Fixed
|
### Fixed
|
||||||
- Bad page when user follows prompt to backup seed
|
- Bad page when user follows prompt to backup seed
|
||||||
|
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ android {
|
|||||||
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 47
|
versionCode 48
|
||||||
versionName "1.1.2"
|
versionName "1.1.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.
|
||||||
|
|||||||
139
doc/ios-share-extension-git-commit-guide.md
Normal file
139
doc/ios-share-extension-git-commit-guide.md
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
# iOS Share Extension - Git Commit Guide
|
||||||
|
|
||||||
|
**Date:** 2025-01-27
|
||||||
|
**Purpose:** Clarify which Xcode manual changes should be committed to the repository
|
||||||
|
|
||||||
|
## Quick Answer
|
||||||
|
|
||||||
|
**YES, most manual Xcode changes SHOULD be committed.** The only exceptions are user-specific settings that are already gitignored.
|
||||||
|
|
||||||
|
## What Gets Modified (and Should Be Committed)
|
||||||
|
|
||||||
|
When you create the Share Extension target and configure App Groups in Xcode, the following files are modified:
|
||||||
|
|
||||||
|
### 1. `ios/App/App.xcodeproj/project.pbxproj` ✅ **COMMIT THIS**
|
||||||
|
|
||||||
|
This is the main Xcode project file that tracks:
|
||||||
|
- **New targets** (Share Extension target)
|
||||||
|
- **File references** (which files belong to which targets)
|
||||||
|
- **Build settings** (compiler flags, deployment targets, etc.)
|
||||||
|
- **Build phases** (compile sources, link frameworks, etc.)
|
||||||
|
- **Capabilities** (App Groups configuration)
|
||||||
|
- **Target dependencies**
|
||||||
|
|
||||||
|
**This file IS tracked in git** (not in `.gitignore`), so changes should be committed.
|
||||||
|
|
||||||
|
### 2. Entitlements Files ✅ **COMMIT THESE**
|
||||||
|
|
||||||
|
When you enable App Groups capability, Xcode creates/modifies:
|
||||||
|
- `ios/App/App/App.entitlements` (for main app)
|
||||||
|
- `ios/App/TimeSafariShareExtension/TimeSafariShareExtension.entitlements` (for extension)
|
||||||
|
|
||||||
|
These files contain the App Group identifiers and should be committed.
|
||||||
|
|
||||||
|
### 3. Share Extension Source Files ✅ **ALREADY COMMITTED**
|
||||||
|
|
||||||
|
The following files are already in the repo:
|
||||||
|
- `ios/App/TimeSafariShareExtension/ShareViewController.swift`
|
||||||
|
- `ios/App/TimeSafariShareExtension/Info.plist`
|
||||||
|
- `ios/App/App/ShareImageBridge.swift`
|
||||||
|
|
||||||
|
These should already be committed (they were created as part of the implementation).
|
||||||
|
|
||||||
|
## What Should NOT Be Committed
|
||||||
|
|
||||||
|
### 1. User-Specific Settings ❌ **ALREADY GITIGNORED**
|
||||||
|
|
||||||
|
These are in `ios/.gitignore`:
|
||||||
|
- `xcuserdata/` - User-specific scheme selections, breakpoints, etc.
|
||||||
|
- `*.xcuserstate` - User's current Xcode state
|
||||||
|
|
||||||
|
### 2. Signing Identities ❌ **USER-SPECIFIC**
|
||||||
|
|
||||||
|
While the **App Groups capability** should be committed (it's in `project.pbxproj` and entitlements), your **personal signing identity/team** is user-specific and Xcode handles this automatically per developer.
|
||||||
|
|
||||||
|
## What Happens When You Commit
|
||||||
|
|
||||||
|
When you commit the changes:
|
||||||
|
|
||||||
|
1. **Other developers** who pull the changes will:
|
||||||
|
- ✅ Get the new Share Extension target automatically
|
||||||
|
- ✅ Get the App Groups capability configuration
|
||||||
|
- ✅ Get file references and build settings
|
||||||
|
- ✅ See the Share Extension in their Xcode project
|
||||||
|
|
||||||
|
2. **They will still need to:**
|
||||||
|
- Configure their own signing team/identity (Xcode prompts for this)
|
||||||
|
- Build the project (which may trigger CocoaPods updates)
|
||||||
|
- But they **won't** need to manually create the target or configure App Groups
|
||||||
|
|
||||||
|
## Step-by-Step: What to Commit
|
||||||
|
|
||||||
|
After completing the Xcode setup steps:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check what changed
|
||||||
|
git status
|
||||||
|
|
||||||
|
# You should see:
|
||||||
|
# - ios/App/App.xcodeproj/project.pbxproj (modified)
|
||||||
|
# - ios/App/App/App.entitlements (new or modified)
|
||||||
|
# - ios/App/TimeSafariShareExtension/TimeSafariShareExtension.entitlements (new)
|
||||||
|
# - Possibly other project-related files
|
||||||
|
|
||||||
|
# Review the changes
|
||||||
|
git diff ios/App/App.xcodeproj/project.pbxproj
|
||||||
|
|
||||||
|
# Commit the changes
|
||||||
|
git add ios/App/App.xcodeproj/project.pbxproj
|
||||||
|
git add ios/App/App/App.entitlements
|
||||||
|
git add ios/App/TimeSafariShareExtension/TimeSafariShareExtension.entitlements
|
||||||
|
git commit -m "Add iOS Share Extension target and App Groups configuration"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
### Merge Conflicts in project.pbxproj
|
||||||
|
|
||||||
|
The `project.pbxproj` file can have merge conflicts because:
|
||||||
|
- It's auto-generated by Xcode
|
||||||
|
- Multiple developers might modify it
|
||||||
|
- It uses UUIDs that can conflict
|
||||||
|
|
||||||
|
**If you get merge conflicts:**
|
||||||
|
1. Open the project in Xcode
|
||||||
|
2. Xcode will often auto-resolve conflicts
|
||||||
|
3. Or manually resolve by keeping both sets of changes
|
||||||
|
4. Test that the project builds
|
||||||
|
|
||||||
|
### Team/Developer IDs
|
||||||
|
|
||||||
|
The `DEVELOPMENT_TEAM` setting in `project.pbxproj` might be user-specific:
|
||||||
|
- Some teams commit this (if everyone uses the same team)
|
||||||
|
- Some teams use `.xcconfig` files to override per developer
|
||||||
|
- Check with your team's practices
|
||||||
|
|
||||||
|
If you see `DEVELOPMENT_TEAM = GM3FS5JQPH;` in the project file, this is already committed, so your team likely commits team IDs.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
After committing, verify that:
|
||||||
|
1. The Share Extension target appears in Xcode for other developers
|
||||||
|
2. App Groups capability is configured
|
||||||
|
3. The project builds successfully
|
||||||
|
4. No user-specific files were accidentally committed
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
| Change Type | Commit? | Reason |
|
||||||
|
|------------|---------|--------|
|
||||||
|
| New target creation | ✅ Yes | Modifies `project.pbxproj` |
|
||||||
|
| App Groups capability | ✅ Yes | Creates/modifies entitlements files |
|
||||||
|
| File target membership | ✅ Yes | Modifies `project.pbxproj` |
|
||||||
|
| Build settings | ✅ Yes | Modifies `project.pbxproj` |
|
||||||
|
| Source files (Swift, plist) | ✅ Yes | Already in repo |
|
||||||
|
| User scheme selections | ❌ No | In `xcuserdata/` (gitignored) |
|
||||||
|
| Personal signing identity | ⚠️ Maybe | Depends on team practice |
|
||||||
|
|
||||||
|
**Bottom line:** Commit all the Xcode project configuration changes. Other developers will get the Share Extension target automatically when they pull, and they'll only need to configure their personal signing settings.
|
||||||
|
|
||||||
283
doc/ios-share-extension-improvements.md
Normal file
283
doc/ios-share-extension-improvements.md
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
# iOS Share Extension Improvements
|
||||||
|
|
||||||
|
**Date:** 2025-11-24
|
||||||
|
**Purpose:** Explore alternatives to improve user experience by eliminating interstitial UI and simplifying app launch mechanism
|
||||||
|
|
||||||
|
## Current Implementation Issues
|
||||||
|
|
||||||
|
1. **Interstitial UI**: Users see `SLComposeServiceViewController` with a "Post" button before the app opens
|
||||||
|
2. **Deep Link Dependency**: App relies on deep link (`timesafari://shared-photo`) to detect shared images, even though data is already in App Group
|
||||||
|
|
||||||
|
## Improvement 1: Skip Interstitial UI
|
||||||
|
|
||||||
|
### Current Approach
|
||||||
|
- Uses `SLComposeServiceViewController` which shows a UI with "Post" button
|
||||||
|
- User must tap "Post" to proceed
|
||||||
|
|
||||||
|
### Alternative: Custom UIViewController (Headless Processing)
|
||||||
|
|
||||||
|
Replace `SLComposeServiceViewController` with a custom `UIViewController` that:
|
||||||
|
- Processes the image immediately in `viewDidLoad`
|
||||||
|
- Shows no UI (or minimal loading indicator)
|
||||||
|
- Opens the app automatically
|
||||||
|
|
||||||
|
**Implementation:**
|
||||||
|
|
||||||
|
```swift
|
||||||
|
import UIKit
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
|
class ShareViewController: UIViewController {
|
||||||
|
|
||||||
|
private let appGroupIdentifier = "group.app.timesafari"
|
||||||
|
private let sharedPhotoBase64Key = "sharedPhotoBase64"
|
||||||
|
private let sharedPhotoFileNameKey = "sharedPhotoFileName"
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
// Process image immediately without showing UI
|
||||||
|
processAndOpenApp()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func processAndOpenApp() {
|
||||||
|
guard let extensionContext = extensionContext,
|
||||||
|
let inputItems = extensionContext.inputItems as? [NSExtensionItem] else {
|
||||||
|
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
processSharedImage(from: inputItems) { [weak self] success in
|
||||||
|
guard let self = self else {
|
||||||
|
self?.extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if success {
|
||||||
|
self.openMainApp()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete immediately - no UI shown
|
||||||
|
self.extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func processSharedImage(from items: [NSExtensionItem], completion: @escaping (Bool) -> Void) {
|
||||||
|
// ... (same implementation as current)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func openMainApp() {
|
||||||
|
guard let url = URL(string: "timesafari://shared-photo") else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var responder: UIResponder? = self
|
||||||
|
while responder != nil {
|
||||||
|
if let application = responder as? UIApplication {
|
||||||
|
application.open(url, options: [:], completionHandler: nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
responder = responder?.next
|
||||||
|
}
|
||||||
|
|
||||||
|
extensionContext?.open(url, completionHandler: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Info.plist Changes:**
|
||||||
|
- Already configured correctly with `NSExtensionPrincipalClass`
|
||||||
|
- No storyboard needed (already removed)
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- ✅ No interstitial UI - app opens immediately
|
||||||
|
- ✅ Faster user experience
|
||||||
|
- ✅ More seamless integration
|
||||||
|
|
||||||
|
**Considerations:**
|
||||||
|
- ⚠️ User has less control (can't cancel easily)
|
||||||
|
- ⚠️ No visual feedback during processing (could add minimal loading indicator)
|
||||||
|
- ⚠️ Apple guidelines: Extensions should provide value even if they don't open the app
|
||||||
|
|
||||||
|
## Improvement 2: Direct App Launch Without Deep Link
|
||||||
|
|
||||||
|
### Current Approach
|
||||||
|
- Share Extension stores data in App Group UserDefaults
|
||||||
|
- Share Extension opens app via deep link (`timesafari://shared-photo`)
|
||||||
|
- App receives deep link → checks App Group → processes image
|
||||||
|
|
||||||
|
### Alternative: App Lifecycle Detection
|
||||||
|
|
||||||
|
Instead of using deep links, the app can check for shared data when it becomes active:
|
||||||
|
|
||||||
|
**Option A: Check on App Activation**
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// In AppDelegate.swift
|
||||||
|
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||||
|
// Check for shared image from Share Extension
|
||||||
|
if let sharedData = getSharedImageData() {
|
||||||
|
// Store in temp file for JS to read
|
||||||
|
writeSharedImageToTempFile(sharedData)
|
||||||
|
|
||||||
|
// Navigate to shared-photo route directly
|
||||||
|
// This would need to be handled in JS layer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option B: Use Notification (More Reliable)**
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// In ShareViewController.swift (after storing data)
|
||||||
|
private func openMainApp() {
|
||||||
|
// Store a flag that image is ready
|
||||||
|
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userDefaults.set(true, forKey: "sharedPhotoReady")
|
||||||
|
userDefaults.synchronize()
|
||||||
|
|
||||||
|
// Open app (can use any URL scheme or even just launch the app)
|
||||||
|
guard let url = URL(string: "timesafari://") else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var responder: UIResponder? = self
|
||||||
|
while responder != nil {
|
||||||
|
if let application = responder as? UIApplication {
|
||||||
|
application.open(url, options: [:], completionHandler: nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
responder = responder?.next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In AppDelegate.swift
|
||||||
|
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||||
|
let appGroupIdentifier = "group.app.timesafari"
|
||||||
|
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if shared photo is ready
|
||||||
|
if userDefaults.bool(forKey: "sharedPhotoReady") {
|
||||||
|
userDefaults.removeObject(forKey: "sharedPhotoReady")
|
||||||
|
userDefaults.synchronize()
|
||||||
|
|
||||||
|
// Process shared image
|
||||||
|
if let sharedData = getSharedImageData() {
|
||||||
|
writeSharedImageToTempFile(sharedData)
|
||||||
|
|
||||||
|
// Trigger JS to check for shared image
|
||||||
|
// This could be done via Capacitor App plugin or custom event
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option C: Check on App Launch (Most Direct)**
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// In AppDelegate.swift
|
||||||
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||||
|
// Check for shared image immediately on launch
|
||||||
|
checkForSharedImageOnLaunch()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||||
|
// Also check when app becomes active (in case it was already running)
|
||||||
|
checkForSharedImageOnLaunch()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func checkForSharedImageOnLaunch() {
|
||||||
|
if let sharedData = getSharedImageData() {
|
||||||
|
writeSharedImageToTempFile(sharedData)
|
||||||
|
|
||||||
|
// Post a notification or use Capacitor to notify JS
|
||||||
|
NotificationCenter.default.post(name: NSNotification.Name("SharedPhotoReady"), object: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**JavaScript Integration:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In main.capacitor.ts
|
||||||
|
import { App } from '@capacitor/app';
|
||||||
|
|
||||||
|
// Listen for app becoming active
|
||||||
|
App.addListener('appStateChange', async ({ isActive }) => {
|
||||||
|
if (isActive) {
|
||||||
|
// Check for shared image when app becomes active
|
||||||
|
await checkAndStoreNativeSharedImage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also check on initial load
|
||||||
|
if (Capacitor.isNativePlatform() && Capacitor.getPlatform() === 'ios') {
|
||||||
|
checkAndStoreNativeSharedImage().then(result => {
|
||||||
|
if (result.success) {
|
||||||
|
// Navigate to shared-photo route
|
||||||
|
router.push('/shared-photo' + (result.fileName ? `?fileName=${result.fileName}` : ''));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- ✅ No deep link routing needed
|
||||||
|
- ✅ More direct data flow
|
||||||
|
- ✅ App can detect shared content even if it was already running
|
||||||
|
- ✅ Simpler URL scheme handling
|
||||||
|
|
||||||
|
**Considerations:**
|
||||||
|
- ⚠️ Need to ensure app checks on both launch and activation
|
||||||
|
- ⚠️ May need to handle race conditions (app launching vs. share extension writing)
|
||||||
|
- ⚠️ Still need some way to open the app (minimal URL scheme still required)
|
||||||
|
|
||||||
|
## Recommended Approach
|
||||||
|
|
||||||
|
**Best of Both Worlds:**
|
||||||
|
|
||||||
|
1. **Use Custom UIViewController** (Improvement 1) - Eliminates interstitial UI
|
||||||
|
2. **Use App Lifecycle Detection** (Improvement 2, Option C) - Direct data flow
|
||||||
|
|
||||||
|
**Combined Implementation:**
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// ShareViewController.swift - Custom UIViewController
|
||||||
|
class ShareViewController: UIViewController {
|
||||||
|
// Process immediately in viewDidLoad
|
||||||
|
// Store data in App Group
|
||||||
|
// Open app with minimal URL (just "timesafari://")
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppDelegate.swift
|
||||||
|
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||||
|
// Check for shared image
|
||||||
|
// If found, write to temp file and let JS handle navigation
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**JavaScript:**
|
||||||
|
```typescript
|
||||||
|
// Check on app activation
|
||||||
|
App.addListener('appStateChange', async ({ isActive }) => {
|
||||||
|
if (isActive) {
|
||||||
|
const result = await checkAndStoreNativeSharedImage();
|
||||||
|
if (result.success) {
|
||||||
|
router.push('/shared-photo' + (result.fileName ? `?fileName=${result.fileName}` : ''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
This approach:
|
||||||
|
- ✅ No interstitial UI
|
||||||
|
- ✅ No deep link routing complexity
|
||||||
|
- ✅ Direct data flow via App Group
|
||||||
|
- ✅ Works whether app is running or launching fresh
|
||||||
|
|
||||||
140
doc/ios-share-extension-setup.md
Normal file
140
doc/ios-share-extension-setup.md
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
# iOS Share Extension Setup Instructions
|
||||||
|
|
||||||
|
**Date:** 2025-01-27
|
||||||
|
**Purpose:** Step-by-step instructions for setting up the iOS Share Extension in Xcode
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Xcode installed
|
||||||
|
- iOS project already set up with Capacitor
|
||||||
|
- Access to Apple Developer account (for App Groups)
|
||||||
|
|
||||||
|
## Step 1: Create Share Extension Target
|
||||||
|
|
||||||
|
1. Open `ios/App/App.xcodeproj` in Xcode
|
||||||
|
2. In the Project Navigator, select the **App** project (top-level item)
|
||||||
|
3. Click the **+** button at the bottom of the Targets list
|
||||||
|
4. Select **iOS** → **Share Extension**
|
||||||
|
5. Click **Next**
|
||||||
|
6. Configure:
|
||||||
|
- **Product Name:** `TimeSafariShareExtension`
|
||||||
|
- **Bundle Identifier:** `app.timesafari.shareextension` (must match main app's bundle ID with `.shareextension` suffix)
|
||||||
|
- **Language:** Swift
|
||||||
|
7. Click **Finish**
|
||||||
|
|
||||||
|
## Step 2: Configure Share Extension Files
|
||||||
|
|
||||||
|
The following files have been created in `ios/App/TimeSafariShareExtension/`:
|
||||||
|
|
||||||
|
- `ShareViewController.swift` - Main extension logic
|
||||||
|
- `Info.plist` - Extension configuration
|
||||||
|
|
||||||
|
**Verify these files exist and are added to the Share Extension target.**
|
||||||
|
|
||||||
|
## Step 3: Configure App Groups
|
||||||
|
|
||||||
|
App Groups allow the Share Extension and main app to share data.
|
||||||
|
|
||||||
|
### For Main App Target:
|
||||||
|
|
||||||
|
1. Select the **App** target in Xcode
|
||||||
|
2. Go to **Signing & Capabilities** tab
|
||||||
|
3. Click **+ Capability**
|
||||||
|
4. Select **App Groups**
|
||||||
|
5. Click **+** to add a new group
|
||||||
|
6. Enter: `group.app.timesafari`
|
||||||
|
7. Ensure it's checked/enabled
|
||||||
|
|
||||||
|
### For Share Extension Target:
|
||||||
|
|
||||||
|
1. Select the **TimeSafariShareExtension** target
|
||||||
|
2. Go to **Signing & Capabilities** tab
|
||||||
|
3. Click **+ Capability**
|
||||||
|
4. Select **App Groups**
|
||||||
|
5. Click **+** to add a new group
|
||||||
|
6. Enter: `group.app.timesafari` (same as main app)
|
||||||
|
7. Ensure it's checked/enabled
|
||||||
|
|
||||||
|
**Important:** Both targets must use the **exact same** App Group identifier.
|
||||||
|
|
||||||
|
## Step 4: Configure Share Extension Info.plist
|
||||||
|
|
||||||
|
The `Info.plist` file should already be configured, but verify:
|
||||||
|
|
||||||
|
1. Select `TimeSafariShareExtension/Info.plist` in Xcode
|
||||||
|
2. Ensure it contains:
|
||||||
|
- `NSExtensionPointIdentifier` = `com.apple.share-services`
|
||||||
|
- `NSExtensionPrincipalClass` = `$(PRODUCT_MODULE_NAME).ShareViewController`
|
||||||
|
- `NSExtensionActivationSupportsImageWithMaxCount` = `1`
|
||||||
|
|
||||||
|
## Step 5: Add ShareImageBridge to Main App
|
||||||
|
|
||||||
|
1. The file `ios/App/App/ShareImageBridge.swift` has been created
|
||||||
|
2. Ensure it's added to the **App** target (not the Share Extension target)
|
||||||
|
3. In Xcode, select the file and check the **Target Membership** in the File Inspector
|
||||||
|
|
||||||
|
## Step 6: Build and Test
|
||||||
|
|
||||||
|
1. Select the **App** scheme (not the Share Extension scheme)
|
||||||
|
2. Build and run on a device or simulator
|
||||||
|
3. Open Photos app
|
||||||
|
4. Select an image
|
||||||
|
5. Tap **Share** button
|
||||||
|
6. Look for **TimeSafari Share** in the share sheet
|
||||||
|
7. Select it
|
||||||
|
8. The app should open and navigate to the shared photo view
|
||||||
|
|
||||||
|
## Step 7: Troubleshooting
|
||||||
|
|
||||||
|
### Share Extension doesn't appear in share sheet
|
||||||
|
|
||||||
|
- Verify the Share Extension target builds successfully
|
||||||
|
- Check that `Info.plist` is correctly configured
|
||||||
|
- Ensure the extension's bundle identifier follows the pattern: `{main-app-bundle-id}.shareextension`
|
||||||
|
- Clean build folder (Product → Clean Build Folder)
|
||||||
|
|
||||||
|
### App Group access fails
|
||||||
|
|
||||||
|
- Verify both targets have the same App Group identifier
|
||||||
|
- Check that App Groups capability is enabled for both targets
|
||||||
|
- Ensure you're signed in with a valid Apple Developer account
|
||||||
|
- For development, you may need to enable App Groups in your Apple Developer account
|
||||||
|
|
||||||
|
### Shared image not appearing
|
||||||
|
|
||||||
|
- Check Xcode console for errors
|
||||||
|
- Verify `ShareViewController.swift` is correctly implemented
|
||||||
|
- Ensure the deep link `timesafari://shared-photo` is being handled
|
||||||
|
- Check that the native bridge method is being called
|
||||||
|
|
||||||
|
### Build errors
|
||||||
|
|
||||||
|
- Ensure Swift version matches between targets
|
||||||
|
- Check that all required frameworks are linked
|
||||||
|
- Verify deployment targets match between main app and extension
|
||||||
|
|
||||||
|
## Step 8: Native Bridge Implementation (TODO)
|
||||||
|
|
||||||
|
Currently, the JavaScript code needs a way to call the native `getSharedImageData()` method. This requires one of:
|
||||||
|
|
||||||
|
1. **Option A:** Create a minimal Capacitor plugin
|
||||||
|
2. **Option B:** Use Capacitor's existing bridge mechanisms
|
||||||
|
3. **Option C:** Expose the method via a custom URL scheme parameter
|
||||||
|
|
||||||
|
The current implementation in `main.capacitor.ts` has a placeholder that needs to be completed.
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
After the Share Extension is set up and working:
|
||||||
|
|
||||||
|
1. Complete the native bridge implementation to read from App Group
|
||||||
|
2. Test end-to-end flow: Share image → Extension stores → App reads → Displays
|
||||||
|
3. Implement Android version
|
||||||
|
4. Add error handling and edge cases
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Apple Share Extensions Documentation](https://developer.apple.com/documentation/social)
|
||||||
|
- [App Groups Documentation](https://developer.apple.com/documentation/xcode/configuring-app-groups)
|
||||||
|
- [Capacitor Native Bridge](https://capacitorjs.com/docs/guides/building-plugins)
|
||||||
|
|
||||||
93
doc/ios-share-implementation-status.md
Normal file
93
doc/ios-share-implementation-status.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# iOS Share Extension Implementation Status
|
||||||
|
|
||||||
|
**Date:** 2025-01-27
|
||||||
|
**Status:** In Progress - Native Code Complete, Bridge Pending
|
||||||
|
|
||||||
|
## Completed
|
||||||
|
|
||||||
|
✅ **Share Extension Files Created:**
|
||||||
|
- `ios/App/TimeSafariShareExtension/ShareViewController.swift` - Handles image sharing
|
||||||
|
- `ios/App/TimeSafariShareExtension/Info.plist` - Extension configuration
|
||||||
|
|
||||||
|
✅ **Native Bridge Created:**
|
||||||
|
- `ios/App/App/ShareImageBridge.swift` - Native method to read from App Group
|
||||||
|
|
||||||
|
✅ **JavaScript Integration Started:**
|
||||||
|
- `src/services/nativeShareHandler.ts` - Service to handle native shared images
|
||||||
|
- `src/main.capacitor.ts` - Updated to check for native shared images on deep link
|
||||||
|
|
||||||
|
✅ **Documentation:**
|
||||||
|
- `doc/native-share-target-implementation.md` - Complete implementation guide
|
||||||
|
- `doc/ios-share-extension-setup.md` - Xcode setup instructions
|
||||||
|
|
||||||
|
## Pending
|
||||||
|
|
||||||
|
⚠️ **Xcode Configuration (Manual Steps Required):**
|
||||||
|
1. Create Share Extension target in Xcode
|
||||||
|
2. Configure App Groups for both main app and extension
|
||||||
|
3. Add ShareImageBridge.swift to App target
|
||||||
|
4. Build and test
|
||||||
|
|
||||||
|
⚠️ **JavaScript-Native Bridge:**
|
||||||
|
The current implementation has a placeholder for calling the native `ShareImageBridge.getSharedImageData()` method from JavaScript. This needs to be completed using one of:
|
||||||
|
|
||||||
|
**Option A: Minimal Capacitor Plugin** (Recommended for Option 1)
|
||||||
|
- Create a small plugin that exposes the method
|
||||||
|
- Clean and maintainable
|
||||||
|
- Follows Capacitor patterns
|
||||||
|
|
||||||
|
**Option B: Direct Bridge Call**
|
||||||
|
- Use Capacitor's executePlugin or similar mechanism
|
||||||
|
- Requires understanding Capacitor's internal bridge
|
||||||
|
- Less maintainable
|
||||||
|
|
||||||
|
**Option C: AppDelegate Integration**
|
||||||
|
- Have AppDelegate check on launch and expose via a different mechanism
|
||||||
|
- Workaround approach
|
||||||
|
- Less clean but functional
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Complete Xcode Setup:**
|
||||||
|
- Follow `doc/ios-share-extension-setup.md`
|
||||||
|
- Create Share Extension target
|
||||||
|
- Configure App Groups
|
||||||
|
- Build and verify extension appears in share sheet
|
||||||
|
|
||||||
|
2. **Implement JavaScript-Native Bridge:**
|
||||||
|
- Choose one of the options above
|
||||||
|
- Complete the `checkAndStoreNativeSharedImage()` function in `main.capacitor.ts`
|
||||||
|
- Test end-to-end flow
|
||||||
|
|
||||||
|
3. **Testing:**
|
||||||
|
- Share image from Photos app
|
||||||
|
- Verify Share Extension appears
|
||||||
|
- Verify app opens and displays shared image
|
||||||
|
- Test "Record Gift" and "Save as Profile" flows
|
||||||
|
|
||||||
|
## Current Flow
|
||||||
|
|
||||||
|
1. ✅ User shares image → Share Extension receives
|
||||||
|
2. ✅ Share Extension converts to base64
|
||||||
|
3. ✅ Share Extension stores in App Group UserDefaults
|
||||||
|
4. ✅ Share Extension opens app with `timesafari://shared-photo?fileName=...`
|
||||||
|
5. ⚠️ App receives deep link (handled)
|
||||||
|
6. ⚠️ App checks App Group UserDefaults (bridge needed)
|
||||||
|
7. ⚠️ App stores in temp database (pending bridge)
|
||||||
|
8. ✅ SharedPhotoView reads from temp database (already works)
|
||||||
|
|
||||||
|
## Code Locations
|
||||||
|
|
||||||
|
- **Share Extension:** `ios/App/TimeSafariShareExtension/`
|
||||||
|
- **Native Bridge:** `ios/App/App/ShareImageBridge.swift`
|
||||||
|
- **JavaScript Handler:** `src/services/nativeShareHandler.ts`
|
||||||
|
- **Deep Link Integration:** `src/main.capacitor.ts`
|
||||||
|
- **View Component:** `src/views/SharedPhotoView.vue` (already complete)
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The Share Extension code is complete and ready to use
|
||||||
|
- The main missing piece is the JavaScript-to-native bridge
|
||||||
|
- Once the bridge is complete, the entire flow should work end-to-end
|
||||||
|
- The existing `SharedPhotoView.vue` doesn't need changes - it already handles images from temp storage
|
||||||
|
|
||||||
507
doc/native-share-target-implementation.md
Normal file
507
doc/native-share-target-implementation.md
Normal file
@@ -0,0 +1,507 @@
|
|||||||
|
# Native Share Target Implementation Guide
|
||||||
|
|
||||||
|
**Date:** 2025-01-27
|
||||||
|
**Purpose:** Enable TimeSafari native iOS and Android apps to receive shared images from other apps
|
||||||
|
|
||||||
|
## Current State
|
||||||
|
|
||||||
|
The app currently supports **PWA/web share target** functionality:
|
||||||
|
- Service worker intercepts POST to `/share-target`
|
||||||
|
- Images stored in temp database as base64
|
||||||
|
- `SharedPhotoView.vue` processes and displays shared images
|
||||||
|
|
||||||
|
**This does NOT work for native iOS/Android builds** because:
|
||||||
|
- Service workers don't run in native app contexts
|
||||||
|
- Native platforms use different sharing mechanisms (Share Extensions on iOS, Intent Filters on Android)
|
||||||
|
|
||||||
|
## Required Changes
|
||||||
|
|
||||||
|
### 1. iOS Implementation
|
||||||
|
|
||||||
|
#### 1.1 Create Share Extension Target
|
||||||
|
|
||||||
|
1. Open `ios/App/App.xcodeproj` in Xcode
|
||||||
|
2. File → New → Target
|
||||||
|
3. Select "Share Extension" template
|
||||||
|
4. Name it "TimeSafariShareExtension"
|
||||||
|
5. Bundle Identifier: `app.timesafari.shareextension`
|
||||||
|
6. Language: Swift
|
||||||
|
|
||||||
|
#### 1.2 Configure Share Extension Info.plist
|
||||||
|
|
||||||
|
Add to `ios/App/TimeSafariShareExtension/Info.plist`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<key>NSExtension</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExtensionPointIdentifier</key>
|
||||||
|
<string>com.apple.share-services</string>
|
||||||
|
<key>NSExtensionPrincipalClass</key>
|
||||||
|
<string>$(PRODUCT_MODULE_NAME).ShareViewController</string>
|
||||||
|
<key>NSExtensionActivationRule</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
<key>NSExtensionActivationSupportsFileWithMaxCount</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.3 Implement ShareViewController
|
||||||
|
|
||||||
|
Create `ios/App/TimeSafariShareExtension/ShareViewController.swift`:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
import UIKit
|
||||||
|
import Social
|
||||||
|
import MobileCoreServices
|
||||||
|
import Capacitor
|
||||||
|
|
||||||
|
class ShareViewController: SLComposeServiceViewController {
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
self.title = "Share to TimeSafari"
|
||||||
|
}
|
||||||
|
|
||||||
|
override func isContentValid() -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didSelectPost() {
|
||||||
|
guard let extensionItem = extensionContext?.inputItems.first as? NSExtensionItem,
|
||||||
|
let itemProvider = extensionItem.attachments?.first else {
|
||||||
|
self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle image sharing
|
||||||
|
if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeImage as String) {
|
||||||
|
itemProvider.loadItem(forTypeIdentifier: kUTTypeImage as String, options: nil) { [weak self] (item, error) in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
if let url = item as? URL {
|
||||||
|
// Handle file URL
|
||||||
|
self.handleSharedImage(url: url)
|
||||||
|
} else if let image = item as? UIImage {
|
||||||
|
// Handle UIImage directly
|
||||||
|
self.handleSharedImage(image: image)
|
||||||
|
} else if let data = item as? Data {
|
||||||
|
// Handle image data
|
||||||
|
self.handleSharedImage(data: data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleSharedImage(url: URL? = nil, image: UIImage? = nil, data: Data? = nil) {
|
||||||
|
var imageData: Data?
|
||||||
|
var fileName: String?
|
||||||
|
|
||||||
|
if let url = url {
|
||||||
|
imageData = try? Data(contentsOf: url)
|
||||||
|
fileName = url.lastPathComponent
|
||||||
|
} else if let image = image {
|
||||||
|
imageData = image.jpegData(compressionQuality: 0.8)
|
||||||
|
fileName = "shared-image.jpg"
|
||||||
|
} else if let data = data {
|
||||||
|
imageData = data
|
||||||
|
fileName = "shared-image.jpg"
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let imageData = imageData else {
|
||||||
|
self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to base64
|
||||||
|
let base64String = imageData.base64EncodedString()
|
||||||
|
|
||||||
|
// Store in shared UserDefaults (accessible by main app)
|
||||||
|
let userDefaults = UserDefaults(suiteName: "group.app.timesafari")
|
||||||
|
userDefaults?.set(base64String, forKey: "sharedPhotoBase64")
|
||||||
|
userDefaults?.set(fileName ?? "shared-image.jpg", forKey: "sharedPhotoFileName")
|
||||||
|
userDefaults?.synchronize()
|
||||||
|
|
||||||
|
// Open main app with deep link
|
||||||
|
let url = URL(string: "timesafari://shared-photo?fileName=\(fileName?.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "shared-image.jpg")")!
|
||||||
|
var responder = self as UIResponder?
|
||||||
|
while responder != nil {
|
||||||
|
if let application = responder as? UIApplication {
|
||||||
|
application.open(url, options: [:], completionHandler: nil)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
responder = responder?.next
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close share extension
|
||||||
|
self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func configurationItems() -> [Any]! {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.4 Configure App Groups
|
||||||
|
|
||||||
|
1. In Xcode, select main app target → Signing & Capabilities
|
||||||
|
2. Add "App Groups" capability
|
||||||
|
3. Create group: `group.app.timesafari`
|
||||||
|
4. Repeat for Share Extension target with same group name
|
||||||
|
|
||||||
|
#### 1.5 Update Main App to Read from App Group
|
||||||
|
|
||||||
|
The main app needs to check for shared images on launch. This should be added to `AppDelegate.swift` or handled in JavaScript.
|
||||||
|
|
||||||
|
### 2. Android Implementation
|
||||||
|
|
||||||
|
#### 2.1 Update AndroidManifest.xml
|
||||||
|
|
||||||
|
Add intent filter to `MainActivity` in `android/app/src/main/AndroidManifest.xml`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
... existing attributes ...>
|
||||||
|
|
||||||
|
... existing intent filters ...
|
||||||
|
|
||||||
|
<!-- Share Target Intent Filter -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data android:mimeType="image/*" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- Multiple images support (optional) -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data android:mimeType="image/*" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.2 Handle Intent in MainActivity
|
||||||
|
|
||||||
|
Update `android/app/src/main/java/app/timesafari/MainActivity.java`:
|
||||||
|
|
||||||
|
```java
|
||||||
|
package app.timesafari;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.util.Base64;
|
||||||
|
import android.util.Log;
|
||||||
|
import com.getcapacitor.BridgeActivity;
|
||||||
|
import com.getcapacitor.Plugin;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
|
||||||
|
public class MainActivity extends BridgeActivity {
|
||||||
|
private static final String TAG = "MainActivity";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
handleShareIntent(getIntent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onNewIntent(Intent intent) {
|
||||||
|
super.onNewIntent(intent);
|
||||||
|
setIntent(intent);
|
||||||
|
handleShareIntent(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleShareIntent(Intent intent) {
|
||||||
|
if (intent == null) return;
|
||||||
|
|
||||||
|
String action = intent.getAction();
|
||||||
|
String type = intent.getType();
|
||||||
|
|
||||||
|
if (Intent.ACTION_SEND.equals(action) && type != null && type.startsWith("image/")) {
|
||||||
|
Uri imageUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
|
||||||
|
if (imageUri != null) {
|
||||||
|
handleSharedImage(imageUri, intent.getStringExtra(Intent.EXTRA_TEXT));
|
||||||
|
}
|
||||||
|
} else if (Intent.ACTION_SEND_MULTIPLE.equals(action) && type != null && type.startsWith("image/")) {
|
||||||
|
// Handle multiple images (optional - for now just take first)
|
||||||
|
java.util.ArrayList<Uri> imageUris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
|
||||||
|
if (imageUris != null && !imageUris.isEmpty()) {
|
||||||
|
handleSharedImage(imageUris.get(0), null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleSharedImage(Uri imageUri, String fileName) {
|
||||||
|
try {
|
||||||
|
// Read image data
|
||||||
|
InputStream inputStream = getContentResolver().openInputStream(imageUri);
|
||||||
|
if (inputStream == null) {
|
||||||
|
Log.e(TAG, "Failed to open input stream for shared image");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
||||||
|
byte[] data = new byte[8192];
|
||||||
|
int nRead;
|
||||||
|
while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
|
||||||
|
buffer.write(data, 0, nRead);
|
||||||
|
}
|
||||||
|
buffer.flush();
|
||||||
|
byte[] imageBytes = buffer.toByteArray();
|
||||||
|
|
||||||
|
// Convert to base64
|
||||||
|
String base64String = Base64.encodeToString(imageBytes, Base64.NO_WRAP);
|
||||||
|
|
||||||
|
// Extract filename from URI or use default
|
||||||
|
String actualFileName = fileName;
|
||||||
|
if (actualFileName == null || actualFileName.isEmpty()) {
|
||||||
|
String path = imageUri.getPath();
|
||||||
|
if (path != null) {
|
||||||
|
int lastSlash = path.lastIndexOf('/');
|
||||||
|
if (lastSlash >= 0 && lastSlash < path.length() - 1) {
|
||||||
|
actualFileName = path.substring(lastSlash + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (actualFileName == null || actualFileName.isEmpty()) {
|
||||||
|
actualFileName = "shared-image.jpg";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store in SharedPreferences (accessible by JavaScript via Capacitor)
|
||||||
|
android.content.SharedPreferences prefs = getSharedPreferences("TimeSafariShared", MODE_PRIVATE);
|
||||||
|
android.content.SharedPreferences.Editor editor = prefs.edit();
|
||||||
|
editor.putString("sharedPhotoBase64", base64String);
|
||||||
|
editor.putString("sharedPhotoFileName", actualFileName);
|
||||||
|
editor.apply();
|
||||||
|
|
||||||
|
// Trigger JavaScript event or navigate to shared-photo route
|
||||||
|
// This will be handled by JavaScript checking for shared data on app launch
|
||||||
|
Log.d(TAG, "Shared image stored, filename: " + actualFileName);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error handling shared image", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.3 Add Required Permissions
|
||||||
|
|
||||||
|
Ensure `AndroidManifest.xml` has:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <!-- Android 13+ -->
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. JavaScript Layer Updates
|
||||||
|
|
||||||
|
#### 3.1 Create Native Share Handler
|
||||||
|
|
||||||
|
Create `src/services/nativeShareHandler.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* Native Share Handler
|
||||||
|
* Handles shared images from native iOS and Android platforms
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Capacitor } from "@capacitor/core";
|
||||||
|
import { App } from "@capacitor/app";
|
||||||
|
import { Filesystem, Directory, Encoding } from "@capacitor/filesystem";
|
||||||
|
import { logger } from "../utils/logger";
|
||||||
|
import { SHARED_PHOTO_BASE64_KEY } from "../libs/util";
|
||||||
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for shared images from native platforms and store in temp database
|
||||||
|
*/
|
||||||
|
export async function checkForNativeSharedImage(
|
||||||
|
platformService: InstanceType<typeof PlatformServiceMixin>
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (!Capacitor.isNativePlatform()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (Capacitor.getPlatform() === "ios") {
|
||||||
|
return await checkIOSSharedImage(platformService);
|
||||||
|
} else if (Capacitor.getPlatform() === "android") {
|
||||||
|
return await checkAndroidSharedImage(platformService);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error checking for native shared image:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for shared image on iOS (from App Group UserDefaults)
|
||||||
|
*/
|
||||||
|
async function checkIOSSharedImage(
|
||||||
|
platformService: InstanceType<typeof PlatformServiceMixin>
|
||||||
|
): Promise<boolean> {
|
||||||
|
// iOS uses App Groups to share data between extension and main app
|
||||||
|
// We need to use a Capacitor plugin or native code to read from App Group
|
||||||
|
// For now, this is a placeholder - requires native plugin implementation
|
||||||
|
|
||||||
|
// Option 1: Use Capacitor plugin to read from App Group
|
||||||
|
// Option 2: Use native code bridge
|
||||||
|
|
||||||
|
logger.debug("Checking for iOS shared image (not yet implemented)");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for shared image on Android (from SharedPreferences)
|
||||||
|
*/
|
||||||
|
async function checkAndroidSharedImage(
|
||||||
|
platformService: InstanceType<typeof PlatformServiceMixin>
|
||||||
|
): Promise<boolean> {
|
||||||
|
// Android stores in SharedPreferences
|
||||||
|
// We need a Capacitor plugin to read from SharedPreferences
|
||||||
|
// For now, this is a placeholder - requires native plugin implementation
|
||||||
|
|
||||||
|
logger.debug("Checking for Android shared image (not yet implemented)");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store shared image in temp database
|
||||||
|
*/
|
||||||
|
async function storeSharedImage(
|
||||||
|
base64Data: string,
|
||||||
|
fileName: string,
|
||||||
|
platformService: InstanceType<typeof PlatformServiceMixin>
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const existing = await platformService.$getTemp(SHARED_PHOTO_BASE64_KEY);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
await platformService.$updateEntity(
|
||||||
|
"temp",
|
||||||
|
{ blobB64: base64Data },
|
||||||
|
"id = ?",
|
||||||
|
[SHARED_PHOTO_BASE64_KEY]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await platformService.$insertEntity(
|
||||||
|
"temp",
|
||||||
|
{ id: SHARED_PHOTO_BASE64_KEY, blobB64: base64Data },
|
||||||
|
["id", "blobB64"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("Stored shared image in temp database");
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error storing shared image:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.2 Update main.capacitor.ts
|
||||||
|
|
||||||
|
Add check for shared images on app launch:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In main.capacitor.ts, after app mount:
|
||||||
|
|
||||||
|
import { checkForNativeSharedImage } from "./services/nativeShareHandler";
|
||||||
|
|
||||||
|
// Check for shared images when app becomes active
|
||||||
|
App.addListener("appStateChange", async (state) => {
|
||||||
|
if (state.isActive) {
|
||||||
|
// Check for native shared images
|
||||||
|
const hasSharedImage = await checkForNativeSharedImage(/* platformService */);
|
||||||
|
if (hasSharedImage) {
|
||||||
|
// Navigate to shared-photo view
|
||||||
|
await router.push({
|
||||||
|
name: "shared-photo",
|
||||||
|
query: { source: "native" }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also check on initial launch
|
||||||
|
App.getLaunchUrl().then((result) => {
|
||||||
|
if (result?.url) {
|
||||||
|
// Handle deep link
|
||||||
|
} else {
|
||||||
|
// Check for shared image
|
||||||
|
checkForNativeSharedImage(/* platformService */).then((hasShared) => {
|
||||||
|
if (hasShared) {
|
||||||
|
router.push({ name: "shared-photo", query: { source: "native" } });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.3 Update SharedPhotoView.vue
|
||||||
|
|
||||||
|
The existing `SharedPhotoView.vue` should work as-is, but we may want to add detection for native vs web sources.
|
||||||
|
|
||||||
|
### 4. Alternative Approach: Capacitor Plugin
|
||||||
|
|
||||||
|
Instead of implementing native code directly, consider creating a Capacitor plugin:
|
||||||
|
|
||||||
|
1. **Create plugin**: `@capacitor-community/share-target` or custom plugin
|
||||||
|
2. **Plugin methods**:
|
||||||
|
- `checkForSharedImage()`: Returns shared image data if available
|
||||||
|
- `clearSharedImage()`: Clears shared image data after processing
|
||||||
|
|
||||||
|
This would be cleaner and more maintainable.
|
||||||
|
|
||||||
|
### 5. Testing Checklist
|
||||||
|
|
||||||
|
- [ ] Test sharing image from Photos app on iOS
|
||||||
|
- [ ] Test sharing image from Gallery app on Android
|
||||||
|
- [ ] Test sharing from other apps (Safari, Chrome, etc.)
|
||||||
|
- [ ] Verify image appears in SharedPhotoView
|
||||||
|
- [ ] Test "Record Gift" flow with shared image
|
||||||
|
- [ ] Test "Save as Profile" flow with shared image
|
||||||
|
- [ ] Test cancel flow
|
||||||
|
- [ ] Verify temp storage cleanup
|
||||||
|
- [ ] Test app launch with shared image pending
|
||||||
|
- [ ] Test app already running when image is shared
|
||||||
|
|
||||||
|
### 6. Implementation Priority
|
||||||
|
|
||||||
|
**Phase 1: Android (Simpler)**
|
||||||
|
1. Update AndroidManifest.xml
|
||||||
|
2. Implement MainActivity intent handling
|
||||||
|
3. Create JavaScript handler
|
||||||
|
4. Test end-to-end
|
||||||
|
|
||||||
|
**Phase 2: iOS (More Complex)**
|
||||||
|
1. Create Share Extension target
|
||||||
|
2. Implement ShareViewController
|
||||||
|
3. Configure App Groups
|
||||||
|
4. Create JavaScript handler
|
||||||
|
5. Test end-to-end
|
||||||
|
|
||||||
|
### 7. Notes
|
||||||
|
|
||||||
|
- **App Groups (iOS)**: Required for sharing data between Share Extension and main app
|
||||||
|
- **SharedPreferences (Android)**: Standard way to share data between app components
|
||||||
|
- **Base64 Encoding**: Both platforms convert images to base64 for JavaScript compatibility
|
||||||
|
- **File Size Limits**: Consider large image handling and memory management
|
||||||
|
- **Permissions**: Android 13+ requires `READ_MEDIA_IMAGES` instead of `READ_EXTERNAL_STORAGE`
|
||||||
|
|
||||||
|
### 8. References
|
||||||
|
|
||||||
|
- [iOS Share Extensions](https://developer.apple.com/documentation/social)
|
||||||
|
- [Android Share Targets](https://developer.android.com/training/sharing/receive)
|
||||||
|
- [Capacitor App Plugin](https://capacitorjs.com/docs/apis/app)
|
||||||
|
- [Capacitor Native Bridge](https://capacitorjs.com/docs/guides/building-plugins)
|
||||||
|
|
||||||
76
doc/xcode-26-cocoapods-workaround.md
Normal file
76
doc/xcode-26-cocoapods-workaround.md
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# Xcode 26 / CocoaPods Compatibility Workaround
|
||||||
|
|
||||||
|
**Date:** 2025-01-27
|
||||||
|
**Issue:** CocoaPods `xcodeproj` gem (1.27.0) doesn't support Xcode 26's project format version 70
|
||||||
|
|
||||||
|
## The Problem
|
||||||
|
|
||||||
|
Xcode 26.1.1 uses project format version 70, but the `xcodeproj` gem (1.27.0) only supports up to version 56. This causes CocoaPods to fail with:
|
||||||
|
|
||||||
|
```
|
||||||
|
ArgumentError - [Xcodeproj] Unable to find compatibility version string for object version `70`.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Solutions
|
||||||
|
|
||||||
|
### Option 1: Temporarily Downgrade Project Format (Recommended for Now)
|
||||||
|
|
||||||
|
**Before running `pod install` or `npm run build:ios`:**
|
||||||
|
|
||||||
|
1. Edit `ios/App/App.xcodeproj/project.pbxproj`
|
||||||
|
2. Change line 6 from: `objectVersion = 70;` to: `objectVersion = 56;`
|
||||||
|
3. Run your build/sync command
|
||||||
|
4. Change it back to: `objectVersion = 70;` (Xcode will likely change it back automatically)
|
||||||
|
|
||||||
|
**Warning:** Xcode may automatically upgrade the format back to 70 when you open the project. This is okay - just repeat the process when needed.
|
||||||
|
|
||||||
|
### Option 2: Wait for xcodeproj Update
|
||||||
|
|
||||||
|
The `xcodeproj` gem maintainers will eventually release a version that supports format 70. You can:
|
||||||
|
- Check for updates: `bundle update xcodeproj`
|
||||||
|
- Monitor: https://github.com/CocoaPods/Xcodeproj/issues
|
||||||
|
|
||||||
|
### Option 3: Use Xcode Directly (Bypass CocoaPods for Now)
|
||||||
|
|
||||||
|
Since the Share Extension is already set up:
|
||||||
|
1. Open the project in Xcode
|
||||||
|
2. Build directly from Xcode (Product → Build)
|
||||||
|
3. Skip `npm run build:ios` for now
|
||||||
|
4. Test the Share Extension functionality
|
||||||
|
|
||||||
|
### Option 4: Automated Workaround (Integrated into Build Script) ✅
|
||||||
|
|
||||||
|
The workaround is now **automatically integrated** into `scripts/build-ios.sh`. When you run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build:ios
|
||||||
|
```
|
||||||
|
|
||||||
|
The build script will:
|
||||||
|
1. Automatically detect if the project format is version 70
|
||||||
|
2. Temporarily downgrade to version 56
|
||||||
|
3. Run `pod install`
|
||||||
|
4. Restore to version 70
|
||||||
|
5. Continue with the build
|
||||||
|
|
||||||
|
**No manual steps required!** The workaround is transparent and only applies when needed.
|
||||||
|
|
||||||
|
To remove the workaround in the future:
|
||||||
|
1. Check if `xcodeproj` gem supports format 70: `bundle exec gem list xcodeproj`
|
||||||
|
2. Test if `pod install` works without the workaround
|
||||||
|
3. If it works, remove the `run_pod_install_with_workaround()` function from `scripts/build-ios.sh`
|
||||||
|
4. Replace it with a simple `pod install` call
|
||||||
|
|
||||||
|
## Current Status
|
||||||
|
|
||||||
|
- ✅ Share Extension target exists
|
||||||
|
- ✅ Share Extension files are in place
|
||||||
|
- ✅ Workaround integrated into build script
|
||||||
|
- ✅ `npm run build:ios` works automatically
|
||||||
|
|
||||||
|
## Recommendation
|
||||||
|
|
||||||
|
**Use `npm run build:ios`** - the workaround is handled automatically. No manual intervention needed.
|
||||||
|
|
||||||
|
Once `xcodeproj` is updated to support format 70, the workaround can be removed from the build script.
|
||||||
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 54;
|
objectVersion = 70;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
@@ -15,8 +15,35 @@
|
|||||||
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
|
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
|
||||||
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
|
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
|
||||||
97EF2DC6FD76C3643D680B8D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 90DCAFB4D8948F7A50C13800 /* Pods_App.framework */; };
|
97EF2DC6FD76C3643D680B8D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 90DCAFB4D8948F7A50C13800 /* Pods_App.framework */; };
|
||||||
|
C86585DF2ED456DE00824752 /* TimeSafariShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = C86585D52ED456DE00824752 /* TimeSafariShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
|
C86585E82ED45A3E00824752 /* ShareImageBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = C86585E72ED45A3D00824752 /* ShareImageBridge.swift */; };
|
||||||
|
C8D7E2CC2ED46A3B00DD738D /* ShareImagePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8D7E2CB2ED46A3B00DD738D /* ShareImagePlugin.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
C86585DD2ED456DE00824752 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 504EC2FC1FED79650016851F /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = C86585D42ED456DE00824752;
|
||||||
|
remoteInfo = TimeSafariShareExtension;
|
||||||
|
};
|
||||||
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
|
C86585E02ED456DE00824752 /* Embed Foundation Extensions */ = {
|
||||||
|
isa = PBXCopyFilesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
dstPath = "";
|
||||||
|
dstSubfolderSpec = 13;
|
||||||
|
files = (
|
||||||
|
C86585DF2ED456DE00824752 /* TimeSafariShareExtension.appex in Embed Foundation Extensions */,
|
||||||
|
);
|
||||||
|
name = "Embed Foundation Extensions";
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
|
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
|
||||||
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
|
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
|
||||||
@@ -28,10 +55,28 @@
|
|||||||
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
|
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
|
||||||
90DCAFB4D8948F7A50C13800 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
90DCAFB4D8948F7A50C13800 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
C86585D52ED456DE00824752 /* TimeSafariShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = TimeSafariShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
C86585E52ED4577F00824752 /* App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = App.entitlements; sourceTree = "<group>"; };
|
||||||
|
C86585E72ED45A3D00824752 /* ShareImageBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareImageBridge.swift; sourceTree = "<group>"; };
|
||||||
|
C8D7E2CB2ED46A3B00DD738D /* ShareImagePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareImagePlugin.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 */
|
||||||
|
|
||||||
|
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
|
C86585E32ED456DE00824752 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
|
||||||
|
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||||
|
membershipExceptions = (
|
||||||
|
Info.plist,
|
||||||
|
);
|
||||||
|
target = C86585D42ED456DE00824752 /* TimeSafariShareExtension */;
|
||||||
|
};
|
||||||
|
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
|
|
||||||
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
|
C86585D62ED456DE00824752 /* TimeSafariShareExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (C86585E32ED456DE00824752 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = TimeSafariShareExtension; sourceTree = "<group>"; };
|
||||||
|
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
504EC3011FED79650016851F /* Frameworks */ = {
|
504EC3011FED79650016851F /* Frameworks */ = {
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
@@ -41,6 +86,13 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
C86585D22ED456DE00824752 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
@@ -56,6 +108,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
504EC3061FED79650016851F /* App */,
|
504EC3061FED79650016851F /* App */,
|
||||||
|
C86585D62ED456DE00824752 /* TimeSafariShareExtension */,
|
||||||
504EC3051FED79650016851F /* Products */,
|
504EC3051FED79650016851F /* Products */,
|
||||||
BA325FFCDCE8D334E5C7AEBE /* Pods */,
|
BA325FFCDCE8D334E5C7AEBE /* Pods */,
|
||||||
4B546315E668C7A13939F417 /* Frameworks */,
|
4B546315E668C7A13939F417 /* Frameworks */,
|
||||||
@@ -66,6 +119,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
504EC3041FED79650016851F /* App.app */,
|
504EC3041FED79650016851F /* App.app */,
|
||||||
|
C86585D52ED456DE00824752 /* TimeSafariShareExtension.appex */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -73,6 +127,9 @@
|
|||||||
504EC3061FED79650016851F /* App */ = {
|
504EC3061FED79650016851F /* App */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
C8D7E2CB2ED46A3B00DD738D /* ShareImagePlugin.swift */,
|
||||||
|
C86585E72ED45A3D00824752 /* ShareImageBridge.swift */,
|
||||||
|
C86585E52ED4577F00824752 /* App.entitlements */,
|
||||||
50379B222058CBB4000EE86E /* capacitor.config.json */,
|
50379B222058CBB4000EE86E /* capacitor.config.json */,
|
||||||
504EC3071FED79650016851F /* AppDelegate.swift */,
|
504EC3071FED79650016851F /* AppDelegate.swift */,
|
||||||
504EC30B1FED79650016851F /* Main.storyboard */,
|
504EC30B1FED79650016851F /* Main.storyboard */,
|
||||||
@@ -108,16 +165,40 @@
|
|||||||
012076E8FFE4BF260A79B034 /* Fix Privacy Manifest */,
|
012076E8FFE4BF260A79B034 /* Fix Privacy Manifest */,
|
||||||
3525031ED1C96EF4CF6E9959 /* [CP] Embed Pods Frameworks */,
|
3525031ED1C96EF4CF6E9959 /* [CP] Embed Pods Frameworks */,
|
||||||
96A7EF592DF3366D00084D51 /* Fix Privacy Manifest */,
|
96A7EF592DF3366D00084D51 /* Fix Privacy Manifest */,
|
||||||
|
C86585E02ED456DE00824752 /* Embed Foundation Extensions */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
dependencies = (
|
dependencies = (
|
||||||
|
C86585DE2ED456DE00824752 /* PBXTargetDependency */,
|
||||||
);
|
);
|
||||||
name = App;
|
name = App;
|
||||||
productName = App;
|
productName = App;
|
||||||
productReference = 504EC3041FED79650016851F /* App.app */;
|
productReference = 504EC3041FED79650016851F /* App.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
};
|
};
|
||||||
|
C86585D42ED456DE00824752 /* TimeSafariShareExtension */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = C86585E42ED456DE00824752 /* Build configuration list for PBXNativeTarget "TimeSafariShareExtension" */;
|
||||||
|
buildPhases = (
|
||||||
|
C86585D12ED456DE00824752 /* Sources */,
|
||||||
|
C86585D22ED456DE00824752 /* Frameworks */,
|
||||||
|
C86585D32ED456DE00824752 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
fileSystemSynchronizedGroups = (
|
||||||
|
C86585D62ED456DE00824752 /* TimeSafariShareExtension */,
|
||||||
|
);
|
||||||
|
name = TimeSafariShareExtension;
|
||||||
|
packageProductDependencies = (
|
||||||
|
);
|
||||||
|
productName = TimeSafariShareExtension;
|
||||||
|
productReference = C86585D52ED456DE00824752 /* TimeSafariShareExtension.appex */;
|
||||||
|
productType = "com.apple.product-type.app-extension";
|
||||||
|
};
|
||||||
/* End PBXNativeTarget section */
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
/* Begin PBXProject section */
|
/* Begin PBXProject section */
|
||||||
@@ -125,7 +206,7 @@
|
|||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
BuildIndependentTargetsInParallel = YES;
|
BuildIndependentTargetsInParallel = YES;
|
||||||
LastSwiftUpdateCheck = 920;
|
LastSwiftUpdateCheck = 2610;
|
||||||
LastUpgradeCheck = 1630;
|
LastUpgradeCheck = 1630;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
504EC3031FED79650016851F = {
|
504EC3031FED79650016851F = {
|
||||||
@@ -133,6 +214,9 @@
|
|||||||
LastSwiftMigration = 1100;
|
LastSwiftMigration = 1100;
|
||||||
ProvisioningStyle = Automatic;
|
ProvisioningStyle = Automatic;
|
||||||
};
|
};
|
||||||
|
C86585D42ED456DE00824752 = {
|
||||||
|
CreatedOnToolsVersion = 26.1.1;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
buildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */;
|
buildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */;
|
||||||
@@ -149,6 +233,7 @@
|
|||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
targets = (
|
targets = (
|
||||||
504EC3031FED79650016851F /* App */,
|
504EC3031FED79650016851F /* App */,
|
||||||
|
C86585D42ED456DE00824752 /* TimeSafariShareExtension */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
@@ -167,6 +252,13 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
C86585D32ED456DE00824752 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXShellScriptBuildPhase section */
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
@@ -253,12 +345,29 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
C8D7E2CC2ED46A3B00DD738D /* ShareImagePlugin.swift in Sources */,
|
||||||
|
C86585E82ED45A3E00824752 /* ShareImageBridge.swift in Sources */,
|
||||||
504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
|
504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
C86585D12ED456DE00824752 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXTargetDependency section */
|
||||||
|
C86585DE2ED456DE00824752 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = C86585D42ED456DE00824752 /* TimeSafariShareExtension */;
|
||||||
|
targetProxy = C86585DD2ED456DE00824752 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
/* Begin PBXVariantGroup section */
|
/* Begin PBXVariantGroup section */
|
||||||
504EC30B1FED79650016851F /* Main.storyboard */ = {
|
504EC30B1FED79650016851F /* Main.storyboard */ = {
|
||||||
isa = PBXVariantGroup;
|
isa = PBXVariantGroup;
|
||||||
@@ -402,8 +511,9 @@
|
|||||||
baseConfigurationReference = EAEC6436E595F7CD3A1C9E96 /* Pods-App.debug.xcconfig */;
|
baseConfigurationReference = EAEC6436E595F7CD3A1C9E96 /* Pods-App.debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 47;
|
CURRENT_PROJECT_VERSION = 48;
|
||||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||||
ENABLE_APP_SANDBOX = NO;
|
ENABLE_APP_SANDBOX = NO;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
@@ -413,7 +523,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.1.2;
|
MARKETING_VERSION = 1.1.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)";
|
||||||
@@ -429,8 +539,9 @@
|
|||||||
baseConfigurationReference = E2E9297D5D02C549106C77F9 /* Pods-App.release.xcconfig */;
|
baseConfigurationReference = E2E9297D5D02C549106C77F9 /* Pods-App.release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 47;
|
CURRENT_PROJECT_VERSION = 48;
|
||||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||||
ENABLE_APP_SANDBOX = NO;
|
ENABLE_APP_SANDBOX = NO;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
@@ -440,7 +551,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.1.2;
|
MARKETING_VERSION = 1.1.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 = "";
|
||||||
@@ -450,6 +561,80 @@
|
|||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
C86585E12ED456DE00824752 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = TimeSafariShareExtension/TimeSafariShareExtension.entitlements;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = TimeSafariShareExtension/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = TimeSafariShareExtension;
|
||||||
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 26.1;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@executable_path/../../Frameworks",
|
||||||
|
);
|
||||||
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari.TimeSafariShareExtension;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||||
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
C86585E22ED456DE00824752 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = TimeSafariShareExtension/TimeSafariShareExtension.entitlements;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = TimeSafariShareExtension/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = TimeSafariShareExtension;
|
||||||
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 26.1;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@executable_path/../../Frameworks",
|
||||||
|
);
|
||||||
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari.TimeSafariShareExtension;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
@@ -471,6 +656,15 @@
|
|||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
|
C86585E42ED456DE00824752 /* Build configuration list for PBXNativeTarget "TimeSafariShareExtension" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
C86585E12ED456DE00824752 /* Debug */,
|
||||||
|
C86585E22ED456DE00824752 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
};
|
};
|
||||||
rootObject = 504EC2FC1FED79650016851F /* Project object */;
|
rootObject = 504EC2FC1FED79650016851F /* Project object */;
|
||||||
|
|||||||
77
ios/App/App.xcodeproj/xcshareddata/xcschemes/App.xcscheme
Normal file
77
ios/App/App.xcodeproj/xcshareddata/xcschemes/App.xcscheme
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1630"
|
||||||
|
version = "1.7">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "504EC3031FED79650016851F"
|
||||||
|
BuildableName = "App.app"
|
||||||
|
BlueprintName = "App"
|
||||||
|
ReferencedContainer = "container:App.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "504EC3031FED79650016851F"
|
||||||
|
BuildableName = "App.app"
|
||||||
|
BlueprintName = "App"
|
||||||
|
ReferencedContainer = "container:App.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "504EC3031FED79650016851F"
|
||||||
|
BuildableName = "App.app"
|
||||||
|
BlueprintName = "App"
|
||||||
|
ReferencedContainer = "container:App.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
10
ios/App/App/App.entitlements
Normal file
10
ios/App/App/App.entitlements
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>group.app.timesafari</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -32,6 +32,56 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
|
|
||||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||||
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||||
|
|
||||||
|
// Check for shared image from Share Extension when app becomes active
|
||||||
|
checkForSharedImageOnActivation()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for shared image when app launches or becomes active
|
||||||
|
* This allows the app to detect shared images without requiring a deep link
|
||||||
|
*/
|
||||||
|
private func checkForSharedImageOnActivation() {
|
||||||
|
let appGroupIdentifier = "group.app.timesafari"
|
||||||
|
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if shared photo is ready
|
||||||
|
if userDefaults.bool(forKey: "sharedPhotoReady") {
|
||||||
|
// Clear the flag
|
||||||
|
userDefaults.removeObject(forKey: "sharedPhotoReady")
|
||||||
|
userDefaults.synchronize()
|
||||||
|
|
||||||
|
// Get and process shared image data
|
||||||
|
if let sharedData = getSharedImageData() {
|
||||||
|
writeSharedImageToTempFile(sharedData)
|
||||||
|
|
||||||
|
// Post notification for JavaScript to handle navigation
|
||||||
|
NotificationCenter.default.post(name: NSNotification.Name("SharedPhotoReady"), object: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write shared image data to temp file for JavaScript to read
|
||||||
|
*/
|
||||||
|
private func writeSharedImageToTempFile(_ sharedData: [String: String]) {
|
||||||
|
let fileManager = FileManager.default
|
||||||
|
guard let documentsDir = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let tempFileURL = documentsDir.appendingPathComponent("timesafari_shared_photo.json")
|
||||||
|
|
||||||
|
let jsonData: [String: String] = [
|
||||||
|
"base64": sharedData["base64"] ?? "",
|
||||||
|
"fileName": sharedData["fileName"] ?? ""
|
||||||
|
]
|
||||||
|
|
||||||
|
if let json = try? JSONSerialization.data(withJSONObject: jsonData, options: []) {
|
||||||
|
try? json.write(to: tempFileURL)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func applicationWillTerminate(_ application: UIApplication) {
|
func applicationWillTerminate(_ application: UIApplication) {
|
||||||
@@ -39,6 +89,33 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
||||||
|
// Check if this is a shared-photo deep link and store image data in a way JS can access
|
||||||
|
if url.scheme == "timesafari" && url.host == "shared-photo" {
|
||||||
|
// Try to get shared image from App Group and store it in a temp file that JS can read
|
||||||
|
// This is a workaround until the plugin is properly registered
|
||||||
|
if let sharedData = getSharedImageData() {
|
||||||
|
// Write to a temp file in the app's Documents directory that JavaScript can read via Filesystem plugin
|
||||||
|
let fileManager = FileManager.default
|
||||||
|
if let documentsDir = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first {
|
||||||
|
let tempFileURL = documentsDir.appendingPathComponent("timesafari_shared_photo.json")
|
||||||
|
|
||||||
|
// Create JSON data
|
||||||
|
let jsonData: [String: String] = [
|
||||||
|
"base64": sharedData["base64"] ?? "",
|
||||||
|
"fileName": sharedData["fileName"] ?? ""
|
||||||
|
]
|
||||||
|
|
||||||
|
if let json = try? JSONSerialization.data(withJSONObject: jsonData, options: []) {
|
||||||
|
do {
|
||||||
|
try json.write(to: tempFileURL)
|
||||||
|
} catch {
|
||||||
|
// Error writing temp file - will be handled by JS layer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Called when the app was launched with a url. Feel free to add additional processing here,
|
// Called when the app was launched with a url. Feel free to add additional processing here,
|
||||||
// but if you want the App API to support tracking app url opens, make sure to keep this call
|
// but if you want the App API to support tracking app url opens, make sure to keep this call
|
||||||
return ApplicationDelegateProxy.shared.application(app, open: url, options: options)
|
return ApplicationDelegateProxy.shared.application(app, open: url, options: options)
|
||||||
@@ -50,5 +127,30 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
// tracking app url opens, make sure to keep this call
|
// tracking app url opens, make sure to keep this call
|
||||||
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
|
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for shared image from Share Extension
|
||||||
|
* Reads from App Group UserDefaults and returns shared image data if available
|
||||||
|
*
|
||||||
|
* @returns Dictionary with "base64" and "fileName" keys, or nil if no shared image
|
||||||
|
*/
|
||||||
|
func getSharedImageData() -> [String: String]? {
|
||||||
|
let appGroupIdentifier = "group.app.timesafari"
|
||||||
|
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let base64 = userDefaults.string(forKey: "sharedPhotoBase64"),
|
||||||
|
let fileName = userDefaults.string(forKey: "sharedPhotoFileName") else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the shared data after reading
|
||||||
|
userDefaults.removeObject(forKey: "sharedPhotoBase64")
|
||||||
|
userDefaults.removeObject(forKey: "sharedPhotoFileName")
|
||||||
|
userDefaults.synchronize()
|
||||||
|
|
||||||
|
return ["base64": base64, "fileName": fileName]
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
48
ios/App/App/ShareImageBridge.swift
Normal file
48
ios/App/App/ShareImageBridge.swift
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Share Image Bridge
|
||||||
|
*
|
||||||
|
* Provides a bridge between JavaScript and native iOS code to access
|
||||||
|
* shared images stored in App Group UserDefaults by the Share Extension.
|
||||||
|
*
|
||||||
|
* This bridge allows the JavaScript layer to read shared image data
|
||||||
|
* that was stored by the Share Extension.
|
||||||
|
*
|
||||||
|
* Note: This class doesn't need Capacitor - it's a simple Swift utility
|
||||||
|
* that reads from App Group UserDefaults. The JavaScript bridge will be
|
||||||
|
* implemented separately.
|
||||||
|
*/
|
||||||
|
@objc(ShareImageBridge)
|
||||||
|
public class ShareImageBridge: NSObject {
|
||||||
|
|
||||||
|
private static let appGroupIdentifier = "group.app.timesafari"
|
||||||
|
private static let sharedPhotoBase64Key = "sharedPhotoBase64"
|
||||||
|
private static let sharedPhotoFileNameKey = "sharedPhotoFileName"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get shared image data from App Group UserDefaults
|
||||||
|
* Called from JavaScript via Capacitor bridge
|
||||||
|
*
|
||||||
|
* @returns Dictionary with "base64" and "fileName" keys, or nil if no shared image
|
||||||
|
*/
|
||||||
|
@objc public static func getSharedImageData() -> [String: String]? {
|
||||||
|
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
|
||||||
|
print("ShareImageBridge: Failed to access App Group UserDefaults")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let base64 = userDefaults.string(forKey: sharedPhotoBase64Key),
|
||||||
|
let fileName = userDefaults.string(forKey: sharedPhotoFileNameKey) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the shared data after reading
|
||||||
|
userDefaults.removeObject(forKey: sharedPhotoBase64Key)
|
||||||
|
userDefaults.removeObject(forKey: sharedPhotoFileNameKey)
|
||||||
|
userDefaults.synchronize()
|
||||||
|
|
||||||
|
return ["base64": base64, "fileName": fileName]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
28
ios/App/App/ShareImagePlugin.swift
Normal file
28
ios/App/App/ShareImagePlugin.swift
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import Foundation
|
||||||
|
import Capacitor
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Share Image Plugin
|
||||||
|
*
|
||||||
|
* Capacitor plugin that exposes ShareImageBridge functionality to JavaScript.
|
||||||
|
* Allows JavaScript to retrieve shared images from App Group UserDefaults.
|
||||||
|
*/
|
||||||
|
@objc(ShareImagePlugin)
|
||||||
|
public class ShareImagePlugin: CAPPlugin {
|
||||||
|
|
||||||
|
@objc func getSharedImageData(_ call: CAPPluginCall) {
|
||||||
|
guard let sharedData = ShareImageBridge.getSharedImageData() else {
|
||||||
|
call.resolve(["success": false, "data": NSNull()])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
call.resolve([
|
||||||
|
"success": true,
|
||||||
|
"data": [
|
||||||
|
"base64": sharedData["base64"] ?? "",
|
||||||
|
"fileName": sharedData["fileName"] ?? ""
|
||||||
|
]
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
21
ios/App/TimeSafariShareExtension/Info.plist
Normal file
21
ios/App/TimeSafariShareExtension/Info.plist
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>NSExtension</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExtensionAttributes</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExtensionActivationRule</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
<key>NSExtensionPointIdentifier</key>
|
||||||
|
<string>com.apple.share-services</string>
|
||||||
|
<key>NSExtensionPrincipalClass</key>
|
||||||
|
<string>$(PRODUCT_MODULE_NAME).ShareViewController</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
146
ios/App/TimeSafariShareExtension/ShareViewController.swift
Normal file
146
ios/App/TimeSafariShareExtension/ShareViewController.swift
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
//
|
||||||
|
// ShareViewController.swift
|
||||||
|
// TimeSafariShareExtension
|
||||||
|
//
|
||||||
|
// Created by Aardimus on 11/24/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
|
class ShareViewController: UIViewController {
|
||||||
|
|
||||||
|
private let appGroupIdentifier = "group.app.timesafari"
|
||||||
|
private let sharedPhotoBase64Key = "sharedPhotoBase64"
|
||||||
|
private let sharedPhotoFileNameKey = "sharedPhotoFileName"
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
// Set a minimal background (transparent or loading indicator)
|
||||||
|
view.backgroundColor = .systemBackground
|
||||||
|
|
||||||
|
// Process image immediately without showing UI
|
||||||
|
processAndOpenApp()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func processAndOpenApp() {
|
||||||
|
// extensionContext is automatically available on UIViewController when used as extension principal class
|
||||||
|
guard let context = extensionContext,
|
||||||
|
let inputItems = context.inputItems as? [NSExtensionItem] else {
|
||||||
|
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
processSharedImage(from: inputItems) { [weak self] success in
|
||||||
|
guard let self = self, let context = self.extensionContext else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if success {
|
||||||
|
// Set flag that shared photo is ready
|
||||||
|
self.setSharedPhotoReadyFlag()
|
||||||
|
// Open the main app (using minimal URL - app will detect shared data on activation)
|
||||||
|
self.openMainApp()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete immediately - no UI shown
|
||||||
|
context.completeRequest(returningItems: [], completionHandler: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setSharedPhotoReadyFlag() {
|
||||||
|
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userDefaults.set(true, forKey: "sharedPhotoReady")
|
||||||
|
userDefaults.synchronize()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func processSharedImage(from items: [NSExtensionItem], completion: @escaping (Bool) -> Void) {
|
||||||
|
// Find the first image attachment
|
||||||
|
for item in items {
|
||||||
|
guard let attachments = item.attachments else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for attachment in attachments {
|
||||||
|
if attachment.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
|
||||||
|
attachment.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { [weak self] (data, error) in
|
||||||
|
guard let self = self else {
|
||||||
|
completion(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let error = error {
|
||||||
|
completion(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle different image data types
|
||||||
|
var imageData: Data?
|
||||||
|
var fileName: String = "shared-image.jpg"
|
||||||
|
|
||||||
|
if let url = data as? URL {
|
||||||
|
// Image provided as file URL
|
||||||
|
imageData = try? Data(contentsOf: url)
|
||||||
|
fileName = url.lastPathComponent
|
||||||
|
} else if let image = data as? UIImage {
|
||||||
|
// Image provided as UIImage
|
||||||
|
imageData = image.jpegData(compressionQuality: 0.9)
|
||||||
|
fileName = "shared-image.jpg"
|
||||||
|
} else if let data = data as? Data {
|
||||||
|
// Image provided as raw Data
|
||||||
|
imageData = data
|
||||||
|
fileName = "shared-image.jpg"
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let finalImageData = imageData else {
|
||||||
|
completion(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to base64
|
||||||
|
let base64String = finalImageData.base64EncodedString()
|
||||||
|
|
||||||
|
// Store in App Group UserDefaults
|
||||||
|
guard let userDefaults = UserDefaults(suiteName: self.appGroupIdentifier) else {
|
||||||
|
completion(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userDefaults.set(base64String, forKey: self.sharedPhotoBase64Key)
|
||||||
|
userDefaults.set(fileName, forKey: self.sharedPhotoFileNameKey)
|
||||||
|
userDefaults.synchronize()
|
||||||
|
|
||||||
|
completion(true)
|
||||||
|
}
|
||||||
|
return // Process only the first image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No image found
|
||||||
|
completion(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func openMainApp() {
|
||||||
|
// Open the main app with minimal URL - app will detect shared data on activation
|
||||||
|
guard let url = URL(string: "timesafari://") else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var responder: UIResponder? = self
|
||||||
|
while responder != nil {
|
||||||
|
if let application = responder as? UIApplication {
|
||||||
|
application.open(url, options: [:], completionHandler: nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
responder = responder?.next
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: use extension context
|
||||||
|
extensionContext?.open(url, completionHandler: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>group.app.timesafari</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
24
package-lock.json
generated
24
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "timesafari",
|
"name": "timesafari",
|
||||||
"version": "1.1.3-beta",
|
"version": "1.1.4-beta",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "timesafari",
|
"name": "timesafari",
|
||||||
"version": "1.1.3-beta",
|
"version": "1.1.4-beta",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor-community/electron": "^5.0.1",
|
"@capacitor-community/electron": "^5.0.1",
|
||||||
"@capacitor-community/sqlite": "6.0.2",
|
"@capacitor-community/sqlite": "6.0.2",
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
"@ethersproject/hdnode": "^5.7.0",
|
"@ethersproject/hdnode": "^5.7.0",
|
||||||
"@ethersproject/wallet": "^5.8.0",
|
"@ethersproject/wallet": "^5.8.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||||
|
"@fortawesome/free-brands-svg-icons": "^7.1.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
||||||
"@fortawesome/vue-fontawesome": "^3.0.6",
|
"@fortawesome/vue-fontawesome": "^3.0.6",
|
||||||
@@ -6789,6 +6790,25 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fortawesome/free-brands-svg-icons": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-9byUd9bgNfthsZAjBl6GxOu1VPHgBuRUP9juI7ZoM98h8xNPTCTagfwUFyYscdZq4Hr7gD1azMfM9s5tIWKZZA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-common-types": "7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fortawesome/free-brands-svg-icons/node_modules/@fortawesome/fontawesome-common-types": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-l/BQM7fYntsCI//du+6sEnHOP6a74UixFyOYUyz2DLMXKx+6DEhfR3F2NYGE45XH1JJuIamacb4IZs9S0ZOWLA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@fortawesome/free-regular-svg-icons": {
|
"node_modules/@fortawesome/free-regular-svg-icons": {
|
||||||
"version": "6.7.2",
|
"version": "6.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.2.tgz",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "timesafari",
|
"name": "timesafari",
|
||||||
"version": "1.1.3-beta",
|
"version": "1.1.4-beta",
|
||||||
"description": "Time Safari Application",
|
"description": "Time Safari Application",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Time Safari Team"
|
"name": "Time Safari Team"
|
||||||
@@ -156,6 +156,7 @@
|
|||||||
"@ethersproject/hdnode": "^5.7.0",
|
"@ethersproject/hdnode": "^5.7.0",
|
||||||
"@ethersproject/wallet": "^5.8.0",
|
"@ethersproject/wallet": "^5.8.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||||
|
"@fortawesome/free-brands-svg-icons": "^7.1.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
||||||
"@fortawesome/vue-fontawesome": "^3.0.6",
|
"@fortawesome/vue-fontawesome": "^3.0.6",
|
||||||
|
|||||||
@@ -404,8 +404,141 @@ elif [ "$BUILD_MODE" = "production" ]; then
|
|||||||
safe_execute "Building Capacitor version (production)" "npm run build:capacitor -- --mode production" || exit 3
|
safe_execute "Building Capacitor version (production)" "npm run build:capacitor -- --mode production" || exit 3
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Step 6: Sync with Capacitor
|
# Step 6: Install CocoaPods dependencies (with Xcode 26 workaround)
|
||||||
safe_execute "Syncing with Capacitor" "npx cap sync ios" || exit 6
|
# ===================================================================
|
||||||
|
# WORKAROUND: Xcode 26 / CocoaPods Compatibility Issue
|
||||||
|
# ===================================================================
|
||||||
|
# Xcode 26 uses project format version 70, but CocoaPods' xcodeproj gem
|
||||||
|
# (1.27.0) only supports up to version 56. This causes pod install to fail.
|
||||||
|
#
|
||||||
|
# This workaround temporarily downgrades the project format to 56, runs
|
||||||
|
# pod install, then restores it to 70. Xcode will automatically upgrade
|
||||||
|
# it back to 70 when opened, which is fine.
|
||||||
|
#
|
||||||
|
# NOTE: Both explicit pod install AND Capacitor sync (which runs pod install
|
||||||
|
# internally) need this workaround. See run_pod_install_with_workaround()
|
||||||
|
# and run_cap_sync_with_workaround() functions below.
|
||||||
|
#
|
||||||
|
# TO REMOVE THIS WORKAROUND IN THE FUTURE:
|
||||||
|
# 1. Check if xcodeproj gem has been updated: bundle exec gem list xcodeproj
|
||||||
|
# 2. Test if pod install works without the workaround
|
||||||
|
# 3. If it works, remove both workaround functions below
|
||||||
|
# 4. Replace with:
|
||||||
|
# - safe_execute "Installing CocoaPods dependencies" "cd ios/App && bundle exec pod install && cd ../.." || exit 6
|
||||||
|
# - safe_execute "Syncing with Capacitor" "npx cap sync ios" || exit 6
|
||||||
|
# 5. Update this comment to indicate the workaround has been removed
|
||||||
|
# ===================================================================
|
||||||
|
run_pod_install_with_workaround() {
|
||||||
|
local PROJECT_FILE="ios/App/App.xcodeproj/project.pbxproj"
|
||||||
|
|
||||||
|
log_info "Installing CocoaPods dependencies (with Xcode 26 workaround)..."
|
||||||
|
|
||||||
|
# Check if project file exists
|
||||||
|
if [ ! -f "$PROJECT_FILE" ]; then
|
||||||
|
log_error "Project file not found: $PROJECT_FILE"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check current format version
|
||||||
|
local current_version=$(grep "objectVersion" "$PROJECT_FILE" | head -1 | grep -o "[0-9]\+" || echo "")
|
||||||
|
|
||||||
|
if [ -z "$current_version" ]; then
|
||||||
|
log_error "Could not determine project format version"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_debug "Current project format version: $current_version"
|
||||||
|
|
||||||
|
# Only apply workaround if format is 70
|
||||||
|
if [ "$current_version" = "70" ]; then
|
||||||
|
log_debug "Applying Xcode 26 workaround: temporarily downgrading to format 56"
|
||||||
|
|
||||||
|
# Downgrade to format 56 (supported by CocoaPods)
|
||||||
|
if ! sed -i '' 's/objectVersion = 70;/objectVersion = 56;/' "$PROJECT_FILE"; then
|
||||||
|
log_error "Failed to downgrade project format"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run pod install
|
||||||
|
log_info "Running pod install..."
|
||||||
|
if ! (cd ios/App && bundle exec pod install && cd ../..); then
|
||||||
|
log_error "pod install failed"
|
||||||
|
# Try to restore format even on failure
|
||||||
|
sed -i '' 's/objectVersion = 56;/objectVersion = 70;/' "$PROJECT_FILE" || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Restore to format 70
|
||||||
|
log_debug "Restoring project format to 70..."
|
||||||
|
if ! sed -i '' 's/objectVersion = 56;/objectVersion = 70;/' "$PROJECT_FILE"; then
|
||||||
|
log_warn "Failed to restore project format to 70 (Xcode will upgrade it automatically)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "CocoaPods dependencies installed successfully"
|
||||||
|
else
|
||||||
|
# Format is not 70, run pod install normally
|
||||||
|
log_debug "Project format is $current_version, running pod install normally"
|
||||||
|
if ! (cd ios/App && bundle exec pod install && cd ../..); then
|
||||||
|
log_error "pod install failed"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
log_success "CocoaPods dependencies installed successfully"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
safe_execute "Installing CocoaPods dependencies" "run_pod_install_with_workaround" || exit 6
|
||||||
|
|
||||||
|
# Step 6.5: Sync with Capacitor (also needs workaround since it runs pod install internally)
|
||||||
|
# Capacitor sync internally runs pod install, so we need to apply the workaround here too
|
||||||
|
run_cap_sync_with_workaround() {
|
||||||
|
local PROJECT_FILE="ios/App/App.xcodeproj/project.pbxproj"
|
||||||
|
|
||||||
|
# Check current format version
|
||||||
|
local current_version=$(grep "objectVersion" "$PROJECT_FILE" | head -1 | grep -o "[0-9]\+" || echo "")
|
||||||
|
|
||||||
|
if [ -z "$current_version" ]; then
|
||||||
|
log_error "Could not determine project format version for Capacitor sync"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Only apply workaround if format is 70
|
||||||
|
if [ "$current_version" = "70" ]; then
|
||||||
|
log_debug "Applying Xcode 26 workaround for Capacitor sync: temporarily downgrading to format 56"
|
||||||
|
|
||||||
|
# Downgrade to format 56 (supported by CocoaPods)
|
||||||
|
if ! sed -i '' 's/objectVersion = 70;/objectVersion = 56;/' "$PROJECT_FILE"; then
|
||||||
|
log_error "Failed to downgrade project format for Capacitor sync"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run Capacitor sync (which will run pod install internally)
|
||||||
|
log_info "Running Capacitor sync..."
|
||||||
|
if ! npx cap sync ios; then
|
||||||
|
log_error "Capacitor sync failed"
|
||||||
|
# Try to restore format even on failure
|
||||||
|
sed -i '' 's/objectVersion = 56;/objectVersion = 70;/' "$PROJECT_FILE" || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Restore to format 70
|
||||||
|
log_debug "Restoring project format to 70 after Capacitor sync..."
|
||||||
|
if ! sed -i '' 's/objectVersion = 56;/objectVersion = 70;/' "$PROJECT_FILE"; then
|
||||||
|
log_warn "Failed to restore project format to 70 (Xcode will upgrade it automatically)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "Capacitor sync completed successfully"
|
||||||
|
else
|
||||||
|
# Format is not 70, run sync normally
|
||||||
|
log_debug "Project format is $current_version, running Capacitor sync normally"
|
||||||
|
if ! npx cap sync ios; then
|
||||||
|
log_error "Capacitor sync failed"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
log_success "Capacitor sync completed successfully"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
safe_execute "Syncing with Capacitor" "run_cap_sync_with_workaround" || exit 6
|
||||||
|
|
||||||
# Step 7: Generate assets
|
# Step 7: Generate assets
|
||||||
safe_execute "Generating assets" "npx capacitor-assets generate --ios" || exit 7
|
safe_execute "Generating assets" "npx capacitor-assets generate --ios" || exit 7
|
||||||
|
|||||||
@@ -483,10 +483,13 @@ export default class GiftedDialog extends Vue {
|
|||||||
image: project.image,
|
image: project.image,
|
||||||
handleId: project.handleId,
|
handleId: project.handleId,
|
||||||
};
|
};
|
||||||
this.receiver = {
|
// Only set receiver to "You" if no receiver has been selected yet
|
||||||
did: this.activeDid,
|
if (!this.receiver || !this.receiver.did) {
|
||||||
name: "You",
|
this.receiver = {
|
||||||
};
|
did: this.activeDid,
|
||||||
|
name: "You",
|
||||||
|
};
|
||||||
|
}
|
||||||
this.firstStep = false;
|
this.firstStep = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
29
src/constants/contacts.ts
Normal file
29
src/constants/contacts.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Constants for contact-related functionality
|
||||||
|
* Created: 2025-11-16
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contact method types with user-friendly labels
|
||||||
|
* Used in: ContactEditView.vue, DIDView.vue
|
||||||
|
*/
|
||||||
|
export const CONTACT_METHOD_TYPES = [
|
||||||
|
{ value: "CELL", label: "Mobile" },
|
||||||
|
{ value: "EMAIL", label: "Email" },
|
||||||
|
{ value: "WHATSAPP", label: "WhatsApp" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type for contact method type values
|
||||||
|
*/
|
||||||
|
export type ContactMethodType = (typeof CONTACT_METHOD_TYPES)[number]["value"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to get label for a contact method type
|
||||||
|
* @param type - The contact method type value (e.g., "CELL", "EMAIL")
|
||||||
|
* @returns The user-friendly label or the original type if not found
|
||||||
|
*/
|
||||||
|
export function getContactMethodLabel(type: string): string {
|
||||||
|
const methodType = CONTACT_METHOD_TYPES.find((m) => m.value === type);
|
||||||
|
return methodType ? methodType.label : type;
|
||||||
|
}
|
||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
faDownload,
|
faDownload,
|
||||||
faEllipsis,
|
faEllipsis,
|
||||||
faEllipsisVertical,
|
faEllipsisVertical,
|
||||||
|
faEnvelope,
|
||||||
faEnvelopeOpenText,
|
faEnvelopeOpenText,
|
||||||
faEraser,
|
faEraser,
|
||||||
faEye,
|
faEye,
|
||||||
@@ -101,6 +102,9 @@ import {
|
|||||||
// these are referenced differently, eg. ":icon='['far', 'star']'" as in ProjectViewView.vue
|
// these are referenced differently, eg. ":icon='['far', 'star']'" as in ProjectViewView.vue
|
||||||
import { faStar as faStarRegular } from "@fortawesome/free-regular-svg-icons";
|
import { faStar as faStarRegular } from "@fortawesome/free-regular-svg-icons";
|
||||||
|
|
||||||
|
// Brand icons
|
||||||
|
import { faWhatsapp } from "@fortawesome/free-brands-svg-icons";
|
||||||
|
|
||||||
// Initialize Font Awesome library with all required icons
|
// Initialize Font Awesome library with all required icons
|
||||||
library.add(
|
library.add(
|
||||||
faArrowDown,
|
faArrowDown,
|
||||||
@@ -140,6 +144,7 @@ library.add(
|
|||||||
faDownload,
|
faDownload,
|
||||||
faEllipsis,
|
faEllipsis,
|
||||||
faEllipsisVertical,
|
faEllipsisVertical,
|
||||||
|
faEnvelope,
|
||||||
faEnvelopeOpenText,
|
faEnvelopeOpenText,
|
||||||
faEraser,
|
faEraser,
|
||||||
faEye,
|
faEye,
|
||||||
@@ -193,6 +198,7 @@ library.add(
|
|||||||
faTriangleExclamation,
|
faTriangleExclamation,
|
||||||
faUser,
|
faUser,
|
||||||
faUsers,
|
faUsers,
|
||||||
|
faWhatsapp,
|
||||||
faXmark,
|
faXmark,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -30,11 +30,15 @@
|
|||||||
|
|
||||||
import { initializeApp } from "./main.common";
|
import { initializeApp } from "./main.common";
|
||||||
import { App as CapacitorApp } from "@capacitor/app";
|
import { App as CapacitorApp } from "@capacitor/app";
|
||||||
|
import { Capacitor } from "@capacitor/core";
|
||||||
|
import { Filesystem, Directory, Encoding } from "@capacitor/filesystem";
|
||||||
import router from "./router";
|
import router from "./router";
|
||||||
import { handleApiError } from "./services/api";
|
import { handleApiError } from "./services/api";
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import { DeepLinkHandler } from "./services/deepLinks";
|
import { DeepLinkHandler } from "./services/deepLinks";
|
||||||
import { logger, safeStringify } from "./utils/logger";
|
import { logger, safeStringify } from "./utils/logger";
|
||||||
|
import { PlatformServiceFactory } from "./services/PlatformServiceFactory";
|
||||||
|
import { SHARED_PHOTO_BASE64_KEY } from "./libs/util";
|
||||||
import "./utils/safeAreaInset";
|
import "./utils/safeAreaInset";
|
||||||
|
|
||||||
logger.log("[Capacitor] 🚀 Starting initialization");
|
logger.log("[Capacitor] 🚀 Starting initialization");
|
||||||
@@ -51,6 +55,92 @@ window.addEventListener("unhandledrejection", (event) => {
|
|||||||
|
|
||||||
const deepLinkHandler = new DeepLinkHandler(router);
|
const deepLinkHandler = new DeepLinkHandler(router);
|
||||||
|
|
||||||
|
// Lock to prevent duplicate processing of shared images
|
||||||
|
let isProcessingSharedImage = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Polls for file existence with exponential backoff
|
||||||
|
* More reliable than hardcoded timeout - checks if file actually exists
|
||||||
|
*
|
||||||
|
* @param filePath - Path to the file to check
|
||||||
|
* @param maxRetries - Maximum number of retry attempts (default: 5)
|
||||||
|
* @param initialDelay - Initial delay in milliseconds (default: 100)
|
||||||
|
* @returns Promise<boolean> - true if file exists, false if max retries reached
|
||||||
|
*/
|
||||||
|
async function pollForFileExistence(
|
||||||
|
filePath: string,
|
||||||
|
maxRetries: number = 5,
|
||||||
|
initialDelay: number = 100,
|
||||||
|
): Promise<boolean> {
|
||||||
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
await Filesystem.stat({
|
||||||
|
path: filePath,
|
||||||
|
directory: Directory.Documents,
|
||||||
|
});
|
||||||
|
// File exists
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
// File doesn't exist yet, wait and retry
|
||||||
|
if (attempt < maxRetries - 1) {
|
||||||
|
// Exponential backoff: 100ms, 200ms, 400ms, 800ms, 1600ms
|
||||||
|
const delay = initialDelay * Math.pow(2, attempt);
|
||||||
|
logger.debug(
|
||||||
|
`[Main] File not found (attempt ${attempt + 1}/${maxRetries}), waiting ${delay}ms...`,
|
||||||
|
);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores shared image data in temp database
|
||||||
|
* Handles clearing old data, converting base64 to data URL, and storing
|
||||||
|
*
|
||||||
|
* @param base64 - Raw base64 string of the image
|
||||||
|
* @param fileName - Optional filename for logging
|
||||||
|
* @returns Promise<void>
|
||||||
|
*/
|
||||||
|
async function storeSharedImageInTempDB(
|
||||||
|
base64: string,
|
||||||
|
fileName?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
|
|
||||||
|
// Clear old image from temp DB first to ensure we get the new one
|
||||||
|
try {
|
||||||
|
await platformService.dbExec("DELETE FROM temp WHERE id = ?", [
|
||||||
|
SHARED_PHOTO_BASE64_KEY,
|
||||||
|
]);
|
||||||
|
logger.debug("[Main] Cleared old shared image from temp DB");
|
||||||
|
} catch (clearError) {
|
||||||
|
logger.debug(
|
||||||
|
"[Main] No old image to clear (or error clearing):",
|
||||||
|
clearError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert raw base64 to data URL format that base64ToBlob expects
|
||||||
|
// base64ToBlob expects format: "data:image/jpeg;base64,/9j/4AAQ..."
|
||||||
|
// Try to detect image type from base64 or default to jpeg
|
||||||
|
let mimeType = "image/jpeg"; // default
|
||||||
|
if (base64.startsWith("/9j/") || base64.startsWith("iVBORw0KGgo")) {
|
||||||
|
// JPEG or PNG
|
||||||
|
mimeType = base64.startsWith("/9j/") ? "image/jpeg" : "image/png";
|
||||||
|
}
|
||||||
|
const dataUrl = `data:${mimeType};base64,${base64}`;
|
||||||
|
|
||||||
|
// Use INSERT OR REPLACE to handle existing records
|
||||||
|
await platformService.dbExec(
|
||||||
|
"INSERT OR REPLACE INTO temp (id, blobB64) VALUES (?, ?)",
|
||||||
|
[SHARED_PHOTO_BASE64_KEY, dataUrl],
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`[Main] Stored shared image: ${fileName || "unknown"}`);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles deep link routing for the application
|
* Handles deep link routing for the application
|
||||||
* Processes URLs in the format timesafari://<route>/<param>
|
* Processes URLs in the format timesafari://<route>/<param>
|
||||||
@@ -67,11 +157,249 @@ const deepLinkHandler = new DeepLinkHandler(router);
|
|||||||
*
|
*
|
||||||
* @throws {Error} If URL format is invalid
|
* @throws {Error} If URL format is invalid
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Check for native shared image from iOS App Group UserDefaults
|
||||||
|
* and store in temp database before routing to shared-photo view
|
||||||
|
*/
|
||||||
|
async function checkAndStoreNativeSharedImage(): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
fileName?: string;
|
||||||
|
}> {
|
||||||
|
// Prevent duplicate processing
|
||||||
|
if (isProcessingSharedImage) {
|
||||||
|
logger.debug(
|
||||||
|
"[Main] ⏸️ Shared image processing already in progress, skipping",
|
||||||
|
);
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
isProcessingSharedImage = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!Capacitor.isNativePlatform() || Capacitor.getPlatform() !== "ios") {
|
||||||
|
isProcessingSharedImage = false;
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("[Main] Checking for iOS shared image from App Group");
|
||||||
|
|
||||||
|
// Use Capacitor's native bridge to call the ShareImagePlugin
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const capacitor = (window as any).Capacitor;
|
||||||
|
|
||||||
|
if (!capacitor || !capacitor.Plugins) {
|
||||||
|
logger.debug("[Main] Capacitor plugins not available");
|
||||||
|
isProcessingSharedImage = false;
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// WORKAROUND: Since the plugin isn't being auto-discovered, use a temp file bridge
|
||||||
|
// AppDelegate writes the shared image data to a temp file, and we read it here
|
||||||
|
const tempFilePath = "timesafari_shared_photo.json";
|
||||||
|
|
||||||
|
// Check if file exists first (more reliable than hardcoded timeout)
|
||||||
|
const fileExists = await pollForFileExistence(tempFilePath);
|
||||||
|
if (fileExists) {
|
||||||
|
try {
|
||||||
|
const fileContent = await Filesystem.readFile({
|
||||||
|
path: tempFilePath,
|
||||||
|
directory: Directory.Documents,
|
||||||
|
encoding: Encoding.UTF8,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fileContent.data) {
|
||||||
|
const sharedData = JSON.parse(fileContent.data as string);
|
||||||
|
const base64 = sharedData.base64;
|
||||||
|
const fileName = sharedData.fileName || "shared-image.jpg";
|
||||||
|
|
||||||
|
if (base64) {
|
||||||
|
// Store in temp database using extracted method
|
||||||
|
logger.info(
|
||||||
|
"[Main] Native shared image found (via temp file), storing in temp DB",
|
||||||
|
);
|
||||||
|
await storeSharedImageInTempDB(base64, fileName);
|
||||||
|
|
||||||
|
// Delete the temp file immediately after reading to prevent re-reading
|
||||||
|
try {
|
||||||
|
await Filesystem.deleteFile({
|
||||||
|
path: tempFilePath,
|
||||||
|
directory: Directory.Documents,
|
||||||
|
});
|
||||||
|
logger.debug("[Main] Deleted temp file after reading");
|
||||||
|
} catch (deleteError) {
|
||||||
|
logger.error("[Main] Failed to delete temp file:", deleteError);
|
||||||
|
}
|
||||||
|
|
||||||
|
isProcessingSharedImage = false;
|
||||||
|
return { success: true, fileName };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (fileError: unknown) {
|
||||||
|
// File exists but can't be read - log and continue to plugin method
|
||||||
|
logger.debug(
|
||||||
|
"[Main] Temp file exists but couldn't be read, trying plugin method",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.debug(
|
||||||
|
"[Main] Temp file not found after polling (this is normal if plugin works)",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: Plugin registration issue - ShareImage plugin is not being auto-discovered
|
||||||
|
// This is a known issue that needs to be resolved. For now, we use the temp file workaround above.
|
||||||
|
|
||||||
|
// Try multiple methods to call the plugin (fallback if temp file method fails)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const plugins = (capacitor as any).Plugins;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const shareImagePlugin = (plugins as any)?.ShareImage;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
let result: any = null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
shareImagePlugin &&
|
||||||
|
typeof shareImagePlugin.getSharedImageData === "function"
|
||||||
|
) {
|
||||||
|
logger.debug("[Main] Using direct plugin method");
|
||||||
|
try {
|
||||||
|
result = await shareImagePlugin.getSharedImageData();
|
||||||
|
} catch (pluginError) {
|
||||||
|
logger.error("[Main] Plugin call failed:", pluginError);
|
||||||
|
isProcessingSharedImage = false;
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Method 2: Use Capacitor's execute method
|
||||||
|
logger.debug(
|
||||||
|
"[Main] Plugin not found directly, trying Capacitor.execute",
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const bridge = (capacitor as any).getBridge?.();
|
||||||
|
if (bridge) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
result = await bridge.execute({
|
||||||
|
pluginId: "ShareImage",
|
||||||
|
methodName: "getSharedImageData",
|
||||||
|
options: {},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Method 3: Try execute on Plugins object
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
if ((plugins as any)?.execute) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
result = await (plugins as any).execute(
|
||||||
|
"ShareImage",
|
||||||
|
"getSharedImageData",
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (executeError) {
|
||||||
|
logger.error("[Main] Execute method failed:", executeError);
|
||||||
|
isProcessingSharedImage = false;
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result || !result.success || !result.data) {
|
||||||
|
logger.debug("[Main] No shared image data found in result");
|
||||||
|
isProcessingSharedImage = false;
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const { base64, fileName } = result.data;
|
||||||
|
|
||||||
|
if (!base64) {
|
||||||
|
logger.debug("[Main] Shared image data missing base64");
|
||||||
|
isProcessingSharedImage = false;
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("[Main] Native shared image found, storing in temp DB");
|
||||||
|
|
||||||
|
// Store in temp database using extracted method
|
||||||
|
await storeSharedImageInTempDB(base64, fileName);
|
||||||
|
|
||||||
|
isProcessingSharedImage = false;
|
||||||
|
return { success: true, fileName: fileName || "shared-image.jpg" };
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("[Main] Error checking for native shared image:", error);
|
||||||
|
isProcessingSharedImage = false;
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleDeepLink = async (data: { url: string }) => {
|
const handleDeepLink = async (data: { url: string }) => {
|
||||||
const { url } = data;
|
const { url } = data;
|
||||||
logger.debug(`[Main] 🌐 Deeplink received from Capacitor: ${url}`);
|
logger.debug(`[Main] 🌐 Deeplink received from Capacitor: ${url}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Handle empty path URLs from share extension (timesafari://)
|
||||||
|
// These are used to open the app, and we should check for shared images
|
||||||
|
const isEmptyPathUrl = url === "timesafari://" || url === "timesafari:///";
|
||||||
|
|
||||||
|
if (
|
||||||
|
isEmptyPathUrl &&
|
||||||
|
Capacitor.isNativePlatform() &&
|
||||||
|
Capacitor.getPlatform() === "ios"
|
||||||
|
) {
|
||||||
|
logger.debug(
|
||||||
|
"[Main] 📸 Empty path URL from share extension, checking for native shared image",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Try to get shared image from App Group and store in temp database
|
||||||
|
// AppDelegate writes the file when the deep link is received, so we may need to retry
|
||||||
|
// The checkAndStoreNativeSharedImage function now uses polling internally, so we just call it once
|
||||||
|
try {
|
||||||
|
const imageResult = await checkAndStoreNativeSharedImage();
|
||||||
|
|
||||||
|
if (imageResult.success) {
|
||||||
|
logger.info(
|
||||||
|
"[Main] ✅ Native shared image found, navigating to shared-photo",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wait for router to be ready
|
||||||
|
await router.isReady();
|
||||||
|
|
||||||
|
// Navigate directly to shared-photo route
|
||||||
|
// Use replace if already on shared-photo to force refresh, otherwise push
|
||||||
|
const fileName = imageResult.fileName || "shared-image.jpg";
|
||||||
|
const isAlreadyOnSharedPhoto =
|
||||||
|
router.currentRoute.value.path === "/shared-photo";
|
||||||
|
|
||||||
|
if (isAlreadyOnSharedPhoto) {
|
||||||
|
// Force refresh by replacing the route
|
||||||
|
await router.replace({
|
||||||
|
path: "/shared-photo",
|
||||||
|
query: { fileName, _refresh: Date.now().toString() }, // Add timestamp to force update
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await router.push({
|
||||||
|
path: "/shared-photo",
|
||||||
|
query: { fileName },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`[Main] ✅ Navigated to /shared-photo?fileName=${fileName}`,
|
||||||
|
);
|
||||||
|
return; // Exit early, don't process as deep link
|
||||||
|
} else {
|
||||||
|
logger.debug(
|
||||||
|
"[Main] ℹ️ No native shared image found, ignoring empty path URL",
|
||||||
|
);
|
||||||
|
return; // Exit early, don't process empty path as deep link
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("[Main] Error processing native shared image:", error);
|
||||||
|
// If check fails, don't process as deep link (empty path would fail validation anyway)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Wait for router to be ready
|
// Wait for router to be ready
|
||||||
logger.debug(`[Main] ⏳ Waiting for router to be ready...`);
|
logger.debug(`[Main] ⏳ Waiting for router to be ready...`);
|
||||||
await router.isReady();
|
await router.isReady();
|
||||||
@@ -159,10 +487,76 @@ const registerDeepLinkListener = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for shared image and navigate to shared-photo route if found
|
||||||
|
* This is called when app becomes active (from share extension or app launch)
|
||||||
|
*/
|
||||||
|
async function checkForSharedImageAndNavigate() {
|
||||||
|
if (!Capacitor.isNativePlatform() || Capacitor.getPlatform() !== "ios") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.debug("[Main] 🔍 Checking for shared image on app activation");
|
||||||
|
const imageResult = await checkAndStoreNativeSharedImage();
|
||||||
|
|
||||||
|
if (imageResult.success) {
|
||||||
|
logger.info(
|
||||||
|
"[Main] ✅ Shared image found, navigating to shared-photo route",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wait for router to be ready
|
||||||
|
await router.isReady();
|
||||||
|
|
||||||
|
// Navigate to shared-photo route with fileName if available
|
||||||
|
// Use replace if already on shared-photo to force refresh, otherwise push
|
||||||
|
const fileName = imageResult.fileName || "shared-image.jpg";
|
||||||
|
const isAlreadyOnSharedPhoto =
|
||||||
|
router.currentRoute.value.path === "/shared-photo";
|
||||||
|
|
||||||
|
if (isAlreadyOnSharedPhoto) {
|
||||||
|
// Force refresh by replacing the route
|
||||||
|
await router.replace({
|
||||||
|
path: "/shared-photo",
|
||||||
|
query: { fileName, _refresh: Date.now().toString() }, // Add timestamp to force update
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const route = imageResult.fileName
|
||||||
|
? `/shared-photo?fileName=${encodeURIComponent(imageResult.fileName)}`
|
||||||
|
: "/shared-photo";
|
||||||
|
await router.push(route);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`[Main] ✅ Navigated to /shared-photo?fileName=${fileName}`);
|
||||||
|
} else {
|
||||||
|
logger.debug("[Main] ℹ️ No shared image found");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("[Main] ❌ Error checking for shared image:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logger.log("[Capacitor] 🚀 Mounting app");
|
logger.log("[Capacitor] 🚀 Mounting app");
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
logger.info(`[Main] ✅ App mounted successfully`);
|
logger.info(`[Main] ✅ App mounted successfully`);
|
||||||
|
|
||||||
|
// Check for shared image on initial load (in case app was launched from share extension)
|
||||||
|
if (Capacitor.isNativePlatform() && Capacitor.getPlatform() === "ios") {
|
||||||
|
setTimeout(async () => {
|
||||||
|
await checkForSharedImageAndNavigate();
|
||||||
|
}, 1000); // Small delay to ensure router is ready
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for app state changes to detect when app becomes active
|
||||||
|
if (Capacitor.isNativePlatform() && Capacitor.getPlatform() === "ios") {
|
||||||
|
CapacitorApp.addListener("appStateChange", async ({ isActive }) => {
|
||||||
|
if (isActive) {
|
||||||
|
logger.debug("[Main] 📱 App became active, checking for shared image");
|
||||||
|
await checkForSharedImageAndNavigate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Register deeplink listener after app is mounted
|
// Register deeplink listener after app is mounted
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -55,66 +55,70 @@
|
|||||||
|
|
||||||
<!-- Contact Methods -->
|
<!-- Contact Methods -->
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<h2 class="text-lg font-medium text-gray-700">Contact Methods</h2>
|
<div v-for="(method, index) in contactMethods" :key="index" class="mt-4">
|
||||||
<div
|
<!-- Type and Value Row -->
|
||||||
v-for="(method, index) in contactMethods"
|
<div class="flex gap-2">
|
||||||
:key="index"
|
<div class="flex-none w-32">
|
||||||
class="flex mt-2"
|
<label class="block text-xs font-medium text-gray-700 mb-1">
|
||||||
>
|
Type
|
||||||
<input
|
</label>
|
||||||
v-model="method.label"
|
<select
|
||||||
type="text"
|
v-model="method.type"
|
||||||
class="block w-1/4 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
class="block w-full border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
||||||
placeholder="Label"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
v-model="method.type"
|
|
||||||
type="text"
|
|
||||||
class="block ml-2 w-1/4 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
|
||||||
placeholder="Type"
|
|
||||||
/>
|
|
||||||
<div class="relative">
|
|
||||||
<button
|
|
||||||
class="px-2 py-1 bg-gray-200 rounded-md"
|
|
||||||
@click="toggleDropdown(index)"
|
|
||||||
>
|
|
||||||
<font-awesome icon="caret-down" class="fa-fw" />
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
v-if="dropdownIndex === index"
|
|
||||||
class="absolute bg-white border border-gray-300 rounded-md mt-1"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
|
|
||||||
@click="setMethodType(index, 'CELL')"
|
|
||||||
>
|
>
|
||||||
CELL
|
<option value="">—</option>
|
||||||
</div>
|
<option
|
||||||
<div
|
v-for="methodType in contactMethodTypes"
|
||||||
class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
|
:key="methodType.value"
|
||||||
@click="setMethodType(index, 'EMAIL')"
|
:value="methodType.value"
|
||||||
>
|
>
|
||||||
EMAIL
|
{{ methodType.label }}
|
||||||
</div>
|
</option>
|
||||||
<div
|
</select>
|
||||||
class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
|
|
||||||
@click="setMethodType(index, 'WHATSAPP')"
|
|
||||||
>
|
|
||||||
WHATSAPP
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<label class="block text-xs font-medium text-gray-700 mb-1">
|
||||||
|
Value
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="method.value"
|
||||||
|
type="text"
|
||||||
|
class="block w-full border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
||||||
|
placeholder="Number, email, etc."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="self-end pb-0.5 text-red-500"
|
||||||
|
@click="removeContactMethod(index)"
|
||||||
|
>
|
||||||
|
<font-awesome icon="trash-can" class="fa-fw" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- WhatsApp Help Text -->
|
||||||
|
<div
|
||||||
|
v-if="method.type === 'WHATSAPP'"
|
||||||
|
class="mt-1 ml-[calc(8rem+0.5rem)] text-xs text-gray-600 italic"
|
||||||
|
>
|
||||||
|
Must include country code and only numbers (e.g., 12225551234)
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Label Row -->
|
||||||
|
<div class="mt-2 flex justify-end">
|
||||||
|
<div class="flex-1 ml-[calc(8rem+0.5rem)]">
|
||||||
|
<label class="block text-xs font-medium text-gray-700 mb-1">
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="method.label"
|
||||||
|
type="text"
|
||||||
|
class="block w-full border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
||||||
|
placeholder="Label / Note"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="w-[2.5rem]"></div>
|
||||||
</div>
|
</div>
|
||||||
<input
|
|
||||||
v-model="method.value"
|
|
||||||
type="text"
|
|
||||||
class="block ml-2 w-1/2 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
|
||||||
placeholder="Number, email, etc."
|
|
||||||
/>
|
|
||||||
<button class="ml-2 text-red-500" @click="removeContactMethod(index)">
|
|
||||||
<font-awesome icon="trash-can" class="fa-fw" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<button class="mt-2" @click="addContactMethod">
|
<button class="mt-4" @click="addContactMethod">
|
||||||
<font-awesome
|
<font-awesome
|
||||||
icon="plus"
|
icon="plus"
|
||||||
class="fa-fw px-2 py-2.5 bg-green-500 text-green-100 rounded-full"
|
class="fa-fw px-2 py-2.5 bg-green-500 text-green-100 rounded-full"
|
||||||
@@ -157,6 +161,7 @@ import {
|
|||||||
} from "../constants/notifications";
|
} from "../constants/notifications";
|
||||||
import { Contact, ContactMethod } from "../db/tables/contacts";
|
import { Contact, ContactMethod } from "../db/tables/contacts";
|
||||||
import { AppString } from "../constants/app";
|
import { AppString } from "../constants/app";
|
||||||
|
import { CONTACT_METHOD_TYPES } from "../constants/contacts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contact Edit View Component
|
* Contact Edit View Component
|
||||||
@@ -219,11 +224,11 @@ export default class ContactEditView extends Vue {
|
|||||||
contactNotes = "";
|
contactNotes = "";
|
||||||
/** Array of editable contact methods */
|
/** Array of editable contact methods */
|
||||||
contactMethods: Array<ContactMethod> = [];
|
contactMethods: Array<ContactMethod> = [];
|
||||||
/** Currently open dropdown index, null if none open */
|
|
||||||
dropdownIndex: number | null = null;
|
|
||||||
|
|
||||||
/** App string constants */
|
/** App string constants */
|
||||||
AppString = AppString;
|
AppString = AppString;
|
||||||
|
/** Contact method types for datalist suggestions */
|
||||||
|
contactMethodTypes = CONTACT_METHOD_TYPES;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component lifecycle hook that initializes the contact edit form
|
* Component lifecycle hook that initializes the contact edit form
|
||||||
@@ -280,29 +285,6 @@ export default class ContactEditView extends Vue {
|
|||||||
this.contactMethods.splice(index, 1);
|
this.contactMethods.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggles the type selection dropdown for a contact method
|
|
||||||
*
|
|
||||||
* If the clicked dropdown is already open, closes it.
|
|
||||||
* If another dropdown is open, closes it and opens the clicked one.
|
|
||||||
*
|
|
||||||
* @param index The array index of the method whose dropdown to toggle
|
|
||||||
*/
|
|
||||||
toggleDropdown(index: number) {
|
|
||||||
this.dropdownIndex = this.dropdownIndex === index ? null : index;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the type for a contact method and closes the dropdown
|
|
||||||
*
|
|
||||||
* @param index The array index of the method to update
|
|
||||||
* @param type The new type value (CELL, EMAIL, WHATSAPP)
|
|
||||||
*/
|
|
||||||
setMethodType(index: number, type: string) {
|
|
||||||
this.contactMethods[index].type = type;
|
|
||||||
this.dropdownIndex = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves the edited contact information to the database
|
* Saves the edited contact information to the database
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -42,6 +42,58 @@
|
|||||||
<font-awesome icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
|
<font-awesome icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
|
||||||
</router-link>
|
</router-link>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
<!-- Notes -->
|
||||||
|
<div v-if="contactFromDid.notes" class="mt-3">
|
||||||
|
<p class="text-sm text-slate-700 whitespace-pre-wrap">
|
||||||
|
{{ contactFromDid.notes }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contact Methods -->
|
||||||
|
<div v-if="contactFromDid.contactMethods?.length" class="mt-3">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div
|
||||||
|
v-for="(method, index) in contactFromDid.contactMethods"
|
||||||
|
:key="index"
|
||||||
|
class="flex items-center gap-2 text-sm"
|
||||||
|
>
|
||||||
|
<span class="font-semibold text-slate-600"
|
||||||
|
>{{
|
||||||
|
getContactMethodLabel(method.type) || "(unspecified)"
|
||||||
|
}}:</span
|
||||||
|
>
|
||||||
|
<span class="text-slate-700">{{ method.label }}</span>
|
||||||
|
<span class="text-slate-600">{{ method.value }}</span>
|
||||||
|
<a
|
||||||
|
v-if="method.type === 'CELL'"
|
||||||
|
:href="`sms:${method.value}`"
|
||||||
|
class="ml-2 text-blue-500 hover:text-blue-700"
|
||||||
|
title="Send text message"
|
||||||
|
>
|
||||||
|
<font-awesome icon="message" class="text-base" />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
v-if="method.type === 'EMAIL'"
|
||||||
|
:href="`mailto:${method.value}`"
|
||||||
|
class="ml-2 text-blue-500 hover:text-blue-700"
|
||||||
|
title="Send email"
|
||||||
|
>
|
||||||
|
<font-awesome icon="envelope" class="text-base" />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
v-if="method.type === 'WHATSAPP'"
|
||||||
|
:href="`https://wa.me/${method.value.replace(/\D/g, '')}`"
|
||||||
|
target="_blank"
|
||||||
|
class="ml-2 text-blue-700"
|
||||||
|
title="Send WhatsApp message"
|
||||||
|
>
|
||||||
|
<font-awesome :icon="['fab', 'whatsapp']" class="text-base" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button class="ml-2 mr-2 mt-4" @click="toggleDidDetails">
|
<button class="ml-2 mr-2 mt-4" @click="toggleDidDetails">
|
||||||
Details
|
Details
|
||||||
<font-awesome
|
<font-awesome
|
||||||
@@ -302,6 +354,7 @@ import {
|
|||||||
NOTIFY_CONTACT_INVALID_DID,
|
NOTIFY_CONTACT_INVALID_DID,
|
||||||
} from "@/constants/notifications";
|
} from "@/constants/notifications";
|
||||||
import { THAT_UNNAMED_PERSON } from "@/constants/entities";
|
import { THAT_UNNAMED_PERSON } from "@/constants/entities";
|
||||||
|
import { getContactMethodLabel } from "@/constants/contacts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DIDView Component
|
* DIDView Component
|
||||||
@@ -352,6 +405,7 @@ export default class DIDView extends Vue {
|
|||||||
capitalizeAndInsertSpacesBeforeCaps = capitalizeAndInsertSpacesBeforeCaps;
|
capitalizeAndInsertSpacesBeforeCaps = capitalizeAndInsertSpacesBeforeCaps;
|
||||||
didInfoForContact = didInfoForContact;
|
didInfoForContact = didInfoForContact;
|
||||||
displayAmount = displayAmount;
|
displayAmount = displayAmount;
|
||||||
|
getContactMethodLabel = getContactMethodLabel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes notification helpers
|
* Initializes notification helpers
|
||||||
|
|||||||
@@ -120,7 +120,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue, Watch } from "vue-facing-decorator";
|
||||||
import {
|
import {
|
||||||
RouteLocationNormalizedLoaded,
|
RouteLocationNormalizedLoaded,
|
||||||
RouteLocationRaw,
|
RouteLocationRaw,
|
||||||
@@ -191,7 +191,29 @@ export default class SharedPhotoView extends Vue {
|
|||||||
*/
|
*/
|
||||||
async mounted() {
|
async mounted() {
|
||||||
this.notify = createNotifyHelpers(this.$notify);
|
this.notify = createNotifyHelpers(this.$notify);
|
||||||
|
await this.loadSharedImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Watches for route query changes to reload image when navigating
|
||||||
|
* to the same route with different query parameters (e.g., new fileName)
|
||||||
|
*/
|
||||||
|
@Watch("$route.query.fileName")
|
||||||
|
async onFileNameChange(newFileName: string | undefined) {
|
||||||
|
if (newFileName) {
|
||||||
|
await this.loadSharedImage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the shared image from temporary storage
|
||||||
|
*
|
||||||
|
* Retrieves the shared image data from the temp database, converts it to a blob,
|
||||||
|
* and updates component state. Cleans up temporary storage after successful loading.
|
||||||
|
*
|
||||||
|
* @async
|
||||||
|
*/
|
||||||
|
private async loadSharedImage() {
|
||||||
try {
|
try {
|
||||||
// Get activeDid from active_identity table (single source of truth)
|
// Get activeDid from active_identity table (single source of truth)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@@ -209,6 +231,9 @@ export default class SharedPhotoView extends Vue {
|
|||||||
this.imageFileName = this.$route.query["fileName"] as string;
|
this.imageFileName = this.$route.query["fileName"] as string;
|
||||||
} else {
|
} else {
|
||||||
logger.error("No appropriate image found in temp storage.", temp);
|
logger.error("No appropriate image found in temp storage.", temp);
|
||||||
|
// Clear image state if no temp data found
|
||||||
|
this.imageBlob = undefined;
|
||||||
|
this.imageFileName = undefined;
|
||||||
}
|
}
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
logger.error("Got an error loading an identifier:", err);
|
logger.error("Got an error loading an identifier:", err);
|
||||||
@@ -216,6 +241,9 @@ export default class SharedPhotoView extends Vue {
|
|||||||
NOTIFY_SHARED_PHOTO_LOAD_ERROR.message,
|
NOTIFY_SHARED_PHOTO_LOAD_ERROR.message,
|
||||||
TIMEOUTS.STANDARD,
|
TIMEOUTS.STANDARD,
|
||||||
);
|
);
|
||||||
|
// Clear image state on error
|
||||||
|
this.imageBlob = undefined;
|
||||||
|
this.imageFileName = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,108 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Nearest Neighbors Section -->
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
profile.issuerDid !== activeDid && // only show neighbors if they're not current user
|
||||||
|
profile.issuerDid !== neighbors?.[0]?.did // and they're not directly connected (since there's no in-between)
|
||||||
|
"
|
||||||
|
class="mt-6"
|
||||||
|
>
|
||||||
|
<h2 class="text-lg font-semibold mb-3">Network Connections</h2>
|
||||||
|
|
||||||
|
<div v-if="loadingNeighbors">
|
||||||
|
<div class="flex justify-center items-center py-8">
|
||||||
|
<font-awesome
|
||||||
|
icon="spinner"
|
||||||
|
class="fa-spin-pulse text-2xl text-slate-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="neighborsError"
|
||||||
|
class="bg-red-50 border border-red-300 rounded-md p-4"
|
||||||
|
>
|
||||||
|
<div class="flex items-start gap-2">
|
||||||
|
<font-awesome
|
||||||
|
icon="exclamation-triangle"
|
||||||
|
class="text-red-500 mt-0.5"
|
||||||
|
/>
|
||||||
|
<p class="text-red-700">{{ neighborsError }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div
|
||||||
|
v-for="neighbor in neighbors"
|
||||||
|
:key="neighbor.did"
|
||||||
|
class="bg-slate-50 border border-slate-300 rounded-md"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between gap-3 p-3">
|
||||||
|
<div class="flex items-center gap-2 flex-1 min-w-0">
|
||||||
|
<button
|
||||||
|
title="Copy profile link and expand"
|
||||||
|
class="text-blue-600 flex-shrink-0"
|
||||||
|
@click="onNeighborExpandClick(neighbor.did)"
|
||||||
|
>
|
||||||
|
<font-awesome
|
||||||
|
:icon="
|
||||||
|
expandedNeighborDid === neighbor.did
|
||||||
|
? 'chevron-down'
|
||||||
|
: 'chevron-right'
|
||||||
|
"
|
||||||
|
class="text-sm"
|
||||||
|
/>
|
||||||
|
{{ getNeighborDisplayName(neighbor.did) }}
|
||||||
|
</button>
|
||||||
|
<span :class="getRelationBadgeClass(neighbor.relation)">
|
||||||
|
{{ getRelationLabel(neighbor.relation) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="expandedNeighborDid === neighbor.did"
|
||||||
|
class="border-t border-slate-300 p-3 bg-white"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'did', params: { did: neighbor.did } }"
|
||||||
|
class="text-blue-600 hover:text-blue-800 font-medium underline"
|
||||||
|
>
|
||||||
|
Go to contact info
|
||||||
|
</router-link>
|
||||||
|
and send them the link in your clipboard and ask for an
|
||||||
|
introduction to this person.
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
getNeighborDisplayName(neighbor.did) === '' ||
|
||||||
|
neighborIsNotInContacts(neighbor.did)
|
||||||
|
"
|
||||||
|
class="flex flex-col gap-1 mt-2"
|
||||||
|
>
|
||||||
|
<p class="text-xs text-slate-600">
|
||||||
|
This person is connected to you, but they are not in this
|
||||||
|
device's contacts. Copy this DID link and check on another
|
||||||
|
device or check with different people.
|
||||||
|
</p>
|
||||||
|
<span class="flex items-center gap-1 min-w-0">
|
||||||
|
<span class="text-xs truncate text-slate-600 min-w-0">
|
||||||
|
{{ neighbor.did }}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
title="Copy DID Link"
|
||||||
|
class="text-blue-600 hover:text-blue-800 underline cursor-pointer flex-shrink-0"
|
||||||
|
@click.prevent="onCopyDidClick(neighbor.did)"
|
||||||
|
>
|
||||||
|
<font-awesome icon="copy" class="text-sm" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Map for first coordinates -->
|
<!-- Map for first coordinates -->
|
||||||
<div v-if="hasFirstLocation" class="mt-4">
|
<div v-if="hasFirstLocation" class="mt-4">
|
||||||
<h2 class="text-lg font-semibold">Location</h2>
|
<h2 class="text-lg font-semibold">Location</h2>
|
||||||
@@ -159,7 +261,11 @@ export default class UserProfileView extends Vue {
|
|||||||
activeDid = "";
|
activeDid = "";
|
||||||
allContacts: Array<Contact> = [];
|
allContacts: Array<Contact> = [];
|
||||||
allMyDids: Array<string> = [];
|
allMyDids: Array<string> = [];
|
||||||
|
expandedNeighborDid: string | null = null;
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
|
loadingNeighbors = false;
|
||||||
|
neighbors: Array<{ did: string; relation: string }> = [];
|
||||||
|
neighborsError = "";
|
||||||
partnerApiServer = DEFAULT_PARTNER_API_SERVER;
|
partnerApiServer = DEFAULT_PARTNER_API_SERVER;
|
||||||
profile: UserProfile | null = null;
|
profile: UserProfile | null = null;
|
||||||
|
|
||||||
@@ -183,8 +289,8 @@ export default class UserProfileView extends Vue {
|
|||||||
*/
|
*/
|
||||||
async mounted() {
|
async mounted() {
|
||||||
await this.initializeSettings();
|
await this.initializeSettings();
|
||||||
await this.loadContacts();
|
|
||||||
await this.loadProfile();
|
await this.loadProfile();
|
||||||
|
await this.loadNeighbors();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -199,12 +305,7 @@ export default class UserProfileView extends Vue {
|
|||||||
this.activeDid = activeIdentity.activeDid || "";
|
this.activeDid = activeIdentity.activeDid || "";
|
||||||
|
|
||||||
this.partnerApiServer = settings.partnerApiServer || this.partnerApiServer;
|
this.partnerApiServer = settings.partnerApiServer || this.partnerApiServer;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads all contacts from database
|
|
||||||
*/
|
|
||||||
private async loadContacts() {
|
|
||||||
this.allContacts = await this.$getAllContacts();
|
this.allContacts = await this.$getAllContacts();
|
||||||
this.allMyDids = await retrieveAccountDids();
|
this.allMyDids = await retrieveAccountDids();
|
||||||
}
|
}
|
||||||
@@ -249,23 +350,100 @@ export default class UserProfileView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copies profile link to clipboard
|
* Loads nearest neighbors from partner API
|
||||||
*
|
*
|
||||||
* Creates a deep link to the profile and copies it to the clipboard
|
* Fetches network connections for the profile and displays them
|
||||||
* Shows success notification when completed
|
* with appropriate relation labels
|
||||||
|
*/
|
||||||
|
async loadNeighbors() {
|
||||||
|
const profileId: string = this.$route.params.id as string;
|
||||||
|
if (!profileId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadingNeighbors = true;
|
||||||
|
this.neighborsError = "";
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${this.partnerApiServer}/api/partner/userProfileNearestNeighbors/${encodeURIComponent(profileId)}`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: await getHeaders(this.activeDid),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
const result = await response.json();
|
||||||
|
this.neighbors = result.data;
|
||||||
|
this.neighborsError = "";
|
||||||
|
} else {
|
||||||
|
logger.warn("Failed to load neighbors:", response.status);
|
||||||
|
this.neighbors = [];
|
||||||
|
this.neighborsError = "Failed to load network connections.";
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error loading neighbors:", error);
|
||||||
|
this.neighbors = [];
|
||||||
|
this.neighborsError =
|
||||||
|
"An error occurred while loading network connections.";
|
||||||
|
} finally {
|
||||||
|
this.loadingNeighbors = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies a deep link to the profile to the clipboard
|
||||||
*/
|
*/
|
||||||
async onCopyLinkClick() {
|
async onCopyLinkClick() {
|
||||||
// Use production URL for sharing to avoid localhost issues in development
|
|
||||||
const deepLink = `${APP_SERVER}/deep-link/user-profile/${this.profile?.rowId}`;
|
const deepLink = `${APP_SERVER}/deep-link/user-profile/${this.profile?.rowId}`;
|
||||||
try {
|
try {
|
||||||
await copyToClipboard(deepLink);
|
await copyToClipboard(deepLink);
|
||||||
this.notify.copied("profile link", TIMEOUTS.STANDARD);
|
this.notify.copied("Profile link", TIMEOUTS.STANDARD);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.$logAndConsole(`Error copying profile link: ${error}`, true);
|
this.$logAndConsole(`Error copying profile link: ${error}`, true);
|
||||||
this.notify.error("Failed to copy profile link.");
|
this.notify.error("Failed to copy profile link.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies a deep link to the provided DID to the clipboard
|
||||||
|
*/
|
||||||
|
async onCopyDidClick(did: string) {
|
||||||
|
const deepLink = `${APP_SERVER}/deep-link/did/${encodeURIComponent(did)}`;
|
||||||
|
try {
|
||||||
|
await copyToClipboard(deepLink);
|
||||||
|
this.notify.copied("DID link", TIMEOUTS.STANDARD);
|
||||||
|
} catch (error) {
|
||||||
|
this.$logAndConsole(`Error copying DID link: ${error}`, true);
|
||||||
|
this.notify.error("Failed to copy DID link.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles clicking the expand button next to a neighbor's name
|
||||||
|
* Copies the profile link to clipboard and toggles the expanded section
|
||||||
|
*/
|
||||||
|
async onNeighborExpandClick(did: string) {
|
||||||
|
if (this.expandedNeighborDid === did) {
|
||||||
|
this.expandedNeighborDid = null;
|
||||||
|
// don't copy the link
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the profile link
|
||||||
|
const deepLink = `${APP_SERVER}/deep-link/user-profile/${this.profile?.rowId}`;
|
||||||
|
try {
|
||||||
|
await copyToClipboard(deepLink);
|
||||||
|
this.notify.copied("Profile link", TIMEOUTS.STANDARD);
|
||||||
|
} catch (error) {
|
||||||
|
this.$logAndConsole(`Error copying profile link: ${error}`, true);
|
||||||
|
this.notify.error("Failed to copy profile link.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle the expanded section
|
||||||
|
this.expandedNeighborDid = did;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computed properties for template logic streamlining
|
* Computed properties for template logic streamlining
|
||||||
*/
|
*/
|
||||||
@@ -330,5 +508,64 @@ export default class UserProfileView extends Vue {
|
|||||||
get tileLayerUrl() {
|
get tileLayerUrl() {
|
||||||
return "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png";
|
return "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets display name for a neighbor's DID
|
||||||
|
* Uses didInfo utility to show contact name if available, otherwise DID
|
||||||
|
* @param did - The DID to get display name for
|
||||||
|
* @returns Formatted display name
|
||||||
|
*/
|
||||||
|
getNeighborDisplayName(did: string): string {
|
||||||
|
return this.didInfo(did, this.activeDid, this.allMyDids, this.allContacts);
|
||||||
|
}
|
||||||
|
|
||||||
|
neighborIsNotInContacts(did: string) {
|
||||||
|
return !this.allContacts.some((contact) => contact.did === did);
|
||||||
|
}
|
||||||
|
|
||||||
|
noNeighborsAreInContacts() {
|
||||||
|
return this.neighbors.every(
|
||||||
|
(neighbor) =>
|
||||||
|
!this.allContacts.some((contact) => contact.did === neighbor.did),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets human-readable label for relation type
|
||||||
|
* @param relation - The relation type from API
|
||||||
|
* @returns Display label for the relation
|
||||||
|
*/
|
||||||
|
getRelationLabel(relation: string): string {
|
||||||
|
switch (relation) {
|
||||||
|
case "REGISTERED_BY_YOU":
|
||||||
|
return "Registered by You";
|
||||||
|
case "REGISTERED_YOU":
|
||||||
|
return "Registered You";
|
||||||
|
case "TARGET":
|
||||||
|
return "Yourself";
|
||||||
|
default:
|
||||||
|
return relation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets CSS classes for relation badge styling
|
||||||
|
* @param relation - The relation type from API
|
||||||
|
* @returns CSS class string for badge
|
||||||
|
*/
|
||||||
|
getRelationBadgeClass(relation: string): string {
|
||||||
|
const baseClasses =
|
||||||
|
"text-xs font-semibold px-2 py-1 rounded whitespace-nowrap";
|
||||||
|
switch (relation) {
|
||||||
|
case "REGISTERED_BY_YOU":
|
||||||
|
return `${baseClasses} bg-blue-100 text-blue-700`;
|
||||||
|
case "REGISTERED_YOU":
|
||||||
|
return `${baseClasses} bg-green-100 text-green-700`;
|
||||||
|
case "TARGET":
|
||||||
|
return `${baseClasses} bg-purple-100 text-purple-700`;
|
||||||
|
default:
|
||||||
|
return `${baseClasses} bg-slate-100 text-slate-700`;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user