diff --git a/.gitignore b/.gitignore index 95b9495..b03841c 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ PasswordMaker.apk # Or iOS profile PasswordMaker_App_Store_provisioning.mobileprovision +PasswordMaker_ShareExtension_store_provisioning.mobileprovision *~ *.sw[mnpcod] diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle index 9cde871..b62ff30 100644 --- a/android/app/capacitor.build.gradle +++ b/android/app/capacitor.build.gradle @@ -18,6 +18,7 @@ dependencies { implementation project(':capacitor-haptics') implementation project(':capacitor-keyboard') implementation project(':capacitor-status-bar') + implementation project(':capgo-capacitor-share-target') implementation "androidx.webkit:webkit:1.4.0" } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index aad5601..b7488f6 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -19,6 +19,11 @@ + + + + + Archive_ * _Distribute App_ from the Organizer dialogue. +## iOS provisioning profiles + +It's necessary to use manual ones on the individual team with no real iPhones. +These are git-ignored and a distinct one is needed for the main app and for the +`ShareExtension`. Both require the App Groups capability. + ## Plugins In addition to the Ionic-standard Capacitor plugins and Clipboard (for copying passwords), diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 1380e78..9d56b06 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 48; + objectVersion = 56; objects = { /* Begin PBXBuildFile section */ @@ -16,8 +16,33 @@ 504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; }; 50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; }; 53E6D92696FE42808EDA44F3 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 6B6854B257764378BC1D04DE /* PrivacyInfo.xcprivacy */; }; + 59FA1C572F26D9E300438282 /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 59FA1C4D2F26D9E300438282 /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 59FA1C552F26D9E300438282 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 504EC2FC1FED79650016851F /* Project object */; + proxyType = 1; + remoteGlobalIDString = 59FA1C4C2F26D9E300438282; + remoteInfo = ShareExtension; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 59FA1C442F26D48500438282 /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 59FA1C572F26D9E300438282 /* ShareExtension.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = ""; }; 50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = ""; }; @@ -28,6 +53,8 @@ 504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = ""; }; + 59FA1C4D2F26D9E300438282 /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 59FA1C5C2F26DC8600438282 /* Webful PasswordMaker.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Webful PasswordMaker.entitlements"; sourceTree = ""; }; 6B6854B257764378BC1D04DE /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 9DB92B35A5C556B224182674 /* Pods_Webful_PasswordMaker.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Webful_PasswordMaker.framework; sourceTree = BUILT_PRODUCTS_DIR; }; AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = ""; }; @@ -36,6 +63,20 @@ FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 59FA1C582F26D9E300438282 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 59FA1C4C2F26D9E300438282 /* ShareExtension */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 59FA1C4E2F26D9E300438282 /* ShareExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (59FA1C582F26D9E300438282 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = ShareExtension; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 504EC3011FED79650016851F /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -45,6 +86,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 59FA1C4A2F26D9E300438282 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -59,7 +107,9 @@ 504EC2FB1FED79650016851F = { isa = PBXGroup; children = ( + 59FA1C5C2F26DC8600438282 /* Webful PasswordMaker.entitlements */, 504EC3061FED79650016851F /* App */, + 59FA1C4E2F26D9E300438282 /* ShareExtension */, 504EC3051FED79650016851F /* Products */, 7F8756D8B27F46E3366F6CEA /* Pods */, 27E2DDA53C4D2A4D1A88CE4A /* Frameworks */, @@ -71,6 +121,7 @@ isa = PBXGroup; children = ( 504EC3041FED79650016851F /* Webful PasswordMaker.app */, + 59FA1C4D2F26D9E300438282 /* ShareExtension.appex */, ); name = Products; sourceTree = ""; @@ -114,23 +165,47 @@ 504EC3021FED79650016851F /* Resources */, 9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */, B3D6F66F01657F49B9BFAD17 /* [CP] Copy Pods Resources */, + 59FA1C442F26D48500438282 /* Embed Foundation Extensions */, ); buildRules = ( ); dependencies = ( + 59FA1C562F26D9E300438282 /* PBXTargetDependency */, ); name = "Webful PasswordMaker"; productName = App; productReference = 504EC3041FED79650016851F /* Webful PasswordMaker.app */; productType = "com.apple.product-type.application"; }; + 59FA1C4C2F26D9E300438282 /* ShareExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 59FA1C592F26D9E300438282 /* Build configuration list for PBXNativeTarget "ShareExtension" */; + buildPhases = ( + 59FA1C492F26D9E300438282 /* Sources */, + 59FA1C4A2F26D9E300438282 /* Frameworks */, + 59FA1C4B2F26D9E300438282 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 59FA1C4E2F26D9E300438282 /* ShareExtension */, + ); + name = ShareExtension; + packageProductDependencies = ( + ); + productName = ShareExtension; + productReference = 59FA1C4D2F26D9E300438282 /* ShareExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 504EC2FC1FED79650016851F /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 920; + LastSwiftUpdateCheck = 2620; LastUpgradeCheck = 920; TargetAttributes = { 504EC3031FED79650016851F = { @@ -138,6 +213,9 @@ LastSwiftMigration = 1100; ProvisioningStyle = Manual; }; + 59FA1C4C2F26D9E300438282 = { + CreatedOnToolsVersion = 26.2; + }; }; }; buildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */; @@ -154,6 +232,7 @@ projectRoot = ""; targets = ( 504EC3031FED79650016851F /* Webful PasswordMaker */, + 59FA1C4C2F26D9E300438282 /* ShareExtension */, ); }; /* End PBXProject section */ @@ -173,6 +252,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 59FA1C4B2F26D9E300438282 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -235,8 +321,23 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 59FA1C492F26D9E300438282 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 59FA1C562F26D9E300438282 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 59FA1C4C2F26D9E300438282 /* ShareExtension */; + targetProxy = 59FA1C552F26D9E300438282 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 504EC30B1FED79650016851F /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -359,7 +460,8 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; VALIDATE_PRODUCT = YES; }; name = Release; @@ -369,6 +471,7 @@ baseConfigurationReference = D5054C214B9034F29815F248 /* Pods-Webful PasswordMaker.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = "Webful PasswordMaker.entitlements"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 2.6.2; @@ -376,7 +479,10 @@ "DEVELOPMENT_TEAM[sdk=iphoneos*]" = N88HM25UAZ; INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); MARKETING_VERSION = 2.6.2; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = uk.webful.passwordmaker; @@ -394,6 +500,7 @@ baseConfigurationReference = CF241BE1BA9577E81CFB14AA /* Pods-Webful PasswordMaker.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = "Webful PasswordMaker.entitlements"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 2.6.2; @@ -401,7 +508,10 @@ "DEVELOPMENT_TEAM[sdk=iphoneos*]" = N88HM25UAZ; INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); MARKETING_VERSION = 2.6.2; PRODUCT_BUNDLE_IDENTIFIER = uk.webful.passwordmaker; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -413,6 +523,96 @@ }; name = Release; }; + 59FA1C5A2F26D9E300438282 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = N88HM25UAZ; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ShareExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + 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 = uk.webful.passwordmaker.ShareExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "PasswordMaker ShareExtension store provisioning"; + 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; + }; + 59FA1C5B2F26D9E300438282 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = N88HM25UAZ; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ShareExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + 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 = uk.webful.passwordmaker.ShareExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "PasswordMaker ShareExtension store provisioning"; + 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 */ /* Begin XCConfigurationList section */ @@ -434,6 +634,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 59FA1C592F26D9E300438282 /* Build configuration list for PBXNativeTarget "ShareExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 59FA1C5A2F26D9E300438282 /* Debug */, + 59FA1C5B2F26D9E300438282 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 504EC2FC1FED79650016851F /* Project object */; diff --git a/ios/App/App.xcodeproj/xcshareddata/xcschemes/ShareExtension.xcscheme b/ios/App/App.xcodeproj/xcshareddata/xcschemes/ShareExtension.xcscheme new file mode 100644 index 0000000..d2294b9 --- /dev/null +++ b/ios/App/App.xcodeproj/xcshareddata/xcschemes/ShareExtension.xcscheme @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/App/App.xcodeproj/xcshareddata/xcschemes/Webful PasswordMaker.xcscheme b/ios/App/App.xcodeproj/xcshareddata/xcschemes/Webful PasswordMaker.xcscheme new file mode 100644 index 0000000..447d625 --- /dev/null +++ b/ios/App/App.xcodeproj/xcshareddata/xcschemes/Webful PasswordMaker.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/App/App/Info.plist b/ios/App/App/Info.plist index fbfe6f8..f98927a 100644 --- a/ios/App/App/Info.plist +++ b/ios/App/App/Info.plist @@ -18,6 +18,17 @@ APPL CFBundleShortVersionString $(MARKETING_VERSION) + CFBundleURLTypes + + + CFBundleURLName + uk.webful.passwordmaker + CFBundleURLSchemes + + passwordmaker + + + CFBundleVersion $(CURRENT_PROJECT_VERSION) LSRequiresIPhoneOS diff --git a/ios/App/App/capacitor.config.json b/ios/App/App/capacitor.config.json index 297d3db..1738ebc 100644 --- a/ios/App/App/capacitor.config.json +++ b/ios/App/App/capacitor.config.json @@ -15,6 +15,9 @@ "scheme": "Webful PasswordMaker" }, "plugins": { + "CapacitorShareTarget": { + "appGroupId": "group.urlshare.uk.webful.passwordmaker" + }, "CapacitorSQLite": { "iosDatabaseLocation": "Library/CapacitorDatabase", "iosIsEncryption": true, @@ -39,6 +42,7 @@ "imageName": "Splashscreen" }, "StatusBar": { + "backgroundColor": "#a11692", "style": "LIGHT", "overlaysWebView": false }, @@ -62,6 +66,7 @@ "HapticsPlugin", "KeyboardPlugin", "StatusBarPlugin", + "CapacitorShareTargetPlugin", "CDVPlugin" ] } diff --git a/ios/App/Podfile b/ios/App/Podfile index 4dd4202..052b289 100644 --- a/ios/App/Podfile +++ b/ios/App/Podfile @@ -20,6 +20,7 @@ def capacitor_pods pod 'CapacitorHaptics', :path => '../../node_modules/@capacitor/haptics' pod 'CapacitorKeyboard', :path => '../../node_modules/@capacitor/keyboard' pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar' + pod 'CapgoCapacitorShareTarget', :path => '../../node_modules/@capgo/capacitor-share-target' pod 'CordovaPlugins', :path => '../capacitor-cordova-ios-plugins' pod 'CordovaPluginsResources', :path => '../capacitor-cordova-ios-plugins' end diff --git a/ios/App/Podfile.lock b/ios/App/Podfile.lock index 67cfbbe..942c1da 100644 --- a/ios/App/Podfile.lock +++ b/ios/App/Podfile.lock @@ -23,6 +23,8 @@ PODS: - Capacitor - CapacitorStatusBar (8.0.0): - Capacitor + - CapgoCapacitorShareTarget (8.0.8): + - Capacitor - CordovaPlugins (8.0.1): - CapacitorCordova - CordovaPluginsResources (0.0.105) @@ -46,6 +48,7 @@ DEPENDENCIES: - "CapacitorHaptics (from `../../node_modules/@capacitor/haptics`)" - "CapacitorKeyboard (from `../../node_modules/@capacitor/keyboard`)" - "CapacitorStatusBar (from `../../node_modules/@capacitor/status-bar`)" + - "CapgoCapacitorShareTarget (from `../../node_modules/@capgo/capacitor-share-target`)" - CordovaPlugins (from `../capacitor-cordova-ios-plugins`) - CordovaPluginsResources (from `../capacitor-cordova-ios-plugins`) @@ -78,6 +81,8 @@ EXTERNAL SOURCES: :path: "../../node_modules/@capacitor/keyboard" CapacitorStatusBar: :path: "../../node_modules/@capacitor/status-bar" + CapgoCapacitorShareTarget: + :path: "../../node_modules/@capgo/capacitor-share-target" CordovaPlugins: :path: "../capacitor-cordova-ios-plugins" CordovaPluginsResources: @@ -95,12 +100,13 @@ SPEC CHECKSUMS: CapacitorHaptics: 2079d9fa04c5a71e18663b4722323c304c58245c CapacitorKeyboard: d7868c079a4d5277a3deca27a10a488fcbbb8b69 CapacitorStatusBar: 312e2e67928dfe9514c4a8916a1fd610552f5f35 + CapgoCapacitorShareTarget: 2b8423bc42b17ecff91ff3e3bc04e4b0d77e521e CordovaPlugins: 27ecf72bbe2be80302bac68e70decdd8d3db5b20 CordovaPluginsResources: da93847212bf7b16ba770e4d6c3e7dba820174c7 IONFilesystemLib: 50b9a0f3052a9b40b9bedb43f328a8fe222f9f87 SQLCipher: 712e8416685e8e575b9b0706ffee71678b2fdcf8 ZIPFoundation: ae5b4b813d216d3bf0a148773267fff14bd51d37 -PODFILE CHECKSUM: 49adf4142110c9574528d44f3bfa138a3650a80c +PODFILE CHECKSUM: 548203aa45bb7ef9ecc5adfb3db8b006d9766271 COCOAPODS: 1.16.2 diff --git a/ios/App/ShareExtension/Base.lproj/MainInterface.storyboard b/ios/App/ShareExtension/Base.lproj/MainInterface.storyboard new file mode 100644 index 0000000..286a508 --- /dev/null +++ b/ios/App/ShareExtension/Base.lproj/MainInterface.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/App/ShareExtension/Info.plist b/ios/App/ShareExtension/Info.plist new file mode 100644 index 0000000..827898b --- /dev/null +++ b/ios/App/ShareExtension/Info.plist @@ -0,0 +1,23 @@ + + + + + NSExtension + + NSExtensionAttributes + + NSExtensionActivationRule + + NSExtensionActivationSupportsText + + NSExtensionActivationSupportsWebURLWithMaxCount + 1 + + + NSExtensionMainStoryboard + MainInterface + NSExtensionPointIdentifier + com.apple.share-services + + + diff --git a/ios/App/ShareExtension/ShareExtension.entitlements b/ios/App/ShareExtension/ShareExtension.entitlements new file mode 100644 index 0000000..3ee5836 --- /dev/null +++ b/ios/App/ShareExtension/ShareExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.urlshare.uk.webful.passwordmaker + + + diff --git a/ios/App/ShareExtension/ShareViewController.swift b/ios/App/ShareExtension/ShareViewController.swift new file mode 100644 index 0000000..2d70d64 --- /dev/null +++ b/ios/App/ShareExtension/ShareViewController.swift @@ -0,0 +1,104 @@ +import UIKit +import Social + +class ShareViewController: SLComposeServiceViewController { + // MUST match the appGroupId in capacitor.config.ts + let APP_GROUP_ID = "group.urlshare.uk.webful.passwordmaker" + // Custom URL scheme - registered in main app's Info.plist + let APP_URL_SCHEME = "passwordmaker" + + override func isContentValid() -> Bool { + // Validate that we have some content + return true + } + + override func viewDidLoad() { + super.viewDidLoad() + navigationController?.navigationBar.topItem?.title = "PasswordMaker" + textView.text = "'Post' passes the URL to PasswordMaker" + textView.isEditable = false + } + + override func didSelectPost() { + // Called when the user taps Post + guard let item = extensionContext?.inputItems.first as? NSExtensionItem else { + self.extensionContext?.completeRequest(returningItems: [], completionHandler: nil) + return + } + + Task { + var urlToShare: String? = nil + + // Process attachments - prioritize URL type + if let attachments = item.attachments { + for provider in attachments { + // Handle URLs (most common from Safari) + if provider.hasItemConformingToTypeIdentifier("public.url") { + if let url = try? await provider.loadItem(forTypeIdentifier: "public.url", options: nil) as? URL { + urlToShare = url.absoluteString + break // Got a URL, that's what we want + } + } + } + + // Fallback to plain text if no URL found + if urlToShare == nil { + for provider in attachments { + if provider.hasItemConformingToTypeIdentifier("public.plain-text") { + if let text = try? await provider.loadItem(forTypeIdentifier: "public.plain-text", options: nil) as? String { + urlToShare = text + break + } + } + } + } + } + + guard let textToShare = urlToShare else { + NSLog("ShareExtension: No URL or text found") + self.extensionContext?.completeRequest(returningItems: [], completionHandler: nil) + return + } + + NSLog("ShareExtension: text to share: \(textToShare)") + + // Save to shared UserDefaults + if let userDefaults = UserDefaults(suiteName: APP_GROUP_ID) { + let shareData: [String: Any] = [ + "title": item.attributedTitle?.string ?? "", + "texts": [textToShare], + "files": [] + ] + userDefaults.set(shareData, forKey: "share-target-data") + userDefaults.synchronize() + NSLog("ShareExtension: Saved to UserDefaults") + } else { + NSLog("ShareExtension: Failed to get UserDefaults!") + } + + // Open the main app via URL scheme + if let url = URL(string: "\(APP_URL_SCHEME)://share") { + await self.openURL(url) + } + + self.extensionContext?.completeRequest(returningItems: [], completionHandler: nil) + } + } + + override func configurationItems() -> [Any]! { + // Return any configuration items for the share sheet (optional) + return [] + } + + @MainActor + private func openURL(_ url: URL) async { + var responder: UIResponder? = self as UIResponder + while responder != nil { + if let application = responder as? UIApplication { + await application.open(url) + return + } + responder = responder?.next + } + } +} diff --git a/ios/App/Webful PasswordMaker.entitlements b/ios/App/Webful PasswordMaker.entitlements new file mode 100644 index 0000000..3ee5836 --- /dev/null +++ b/ios/App/Webful PasswordMaker.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.urlshare.uk.webful.passwordmaker + + + diff --git a/package-lock.json b/package-lock.json index 9c0efc0..5ebd3c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "@capacitor/ios": "^8.0.0", "@capacitor/keyboard": "^8.0.0", "@capacitor/status-bar": "^8.0.0", + "@capgo/capacitor-share-target": "^8.0.8", "@ionic/angular": "^8.2.5", "@ionic/storage-angular": "^4.0.0", "@webful/passwordmaker-lib": "~0.1.9", @@ -3549,6 +3550,15 @@ "integrity": "sha512-/C1FUo8/OkKuAT4nCIu/34ny9siNHr9qtFezu4kxm6GY1wNFxrCFWjfYx5C1tUhVGz3fxBABegupkpjXvjCHrw==", "license": "ISC" }, + "node_modules/@capgo/capacitor-share-target": { + "version": "8.0.8", + "resolved": "https://registry.npmjs.org/@capgo/capacitor-share-target/-/capacitor-share-target-8.0.8.tgz", + "integrity": "sha512-bb4uPIgx1IONxivJfZL9Mzg7L387bGGsI3vtHX0FY4wSoDvt71C0xjssy9hhk1Zc2/SWqA9cNFfIZ0Saumbe0A==", + "license": "MPL-2.0", + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", diff --git a/package.json b/package.json index 6e95320..686c02a 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@capacitor/ios": "^8.0.0", "@capacitor/keyboard": "^8.0.0", "@capacitor/status-bar": "^8.0.0", + "@capgo/capacitor-share-target": "^8.0.8", "@ionic/angular": "^8.2.5", "@ionic/storage-angular": "^4.0.0", "@webful/passwordmaker-lib": "~0.1.9", @@ -117,4 +118,4 @@ "Safari >=14", "iOS >=14" ] -} \ No newline at end of file +} diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 2e0a39b..3efde57 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,5 +1,8 @@ import { Component, inject } from '@angular/core'; +import { Capacitor } from '@capacitor/core'; +import { CapacitorShareTarget, ShareReceivedEvent } from '@capgo/capacitor-share-target'; import { Platform } from '@ionic/angular/standalone'; +import { ShareService } from './share.service'; @Component({ selector: 'app-root', @@ -8,6 +11,7 @@ import { Platform } from '@ionic/angular/standalone'; }) export class AppComponent { private platform = inject(Platform); + private shareService = inject(ShareService); constructor() { this.initializeApp(); @@ -20,6 +24,19 @@ export class AppComponent { const prefersDark = window.matchMedia('(prefers-color-scheme: dark)'); this.toggleDarkTheme(prefersDark.matches); prefersDark.addEventListener('change', event => this.toggleDarkTheme(event.matches)); + + if (Capacitor.isNativePlatform()) { + CapacitorShareTarget.addListener('shareReceived', event => this.receiveShareEvent(event)); + } + } + + receiveShareEvent(event: ShareReceivedEvent) { + if (!event.texts || event.texts.length === 0) { + return; + } + + console.log('Received shared text: ', event.texts[0]); + this.shareService.setSharedHost(event.texts[0]); } /** diff --git a/src/app/home/home.page.ts b/src/app/home/home.page.ts index d7fd6da..9fa06f3 100644 --- a/src/app/home/home.page.ts +++ b/src/app/home/home.page.ts @@ -1,4 +1,4 @@ -import { ChangeDetectorRef, Component, NgZone, OnInit, inject } from '@angular/core'; +import { ChangeDetectorRef, Component, NgZone, OnInit, effect, inject } from '@angular/core'; import { Clipboard } from '@capacitor/clipboard'; import { Keyboard } from '@capacitor/keyboard'; import { LoadingController, Platform, ToastController } from '@ionic/angular/standalone'; @@ -10,6 +10,7 @@ import { PasswordsService } from '../passwords.service'; import { Settings } from '../../models/Settings'; import { SettingsAdvanced } from '../../models/SettingsAdvanced'; import { SettingsService } from '../settings.service'; +import { ShareService } from '../share.service'; @Component({ selector: 'app-home', @@ -23,6 +24,7 @@ export class HomePageComponent implements OnInit { private passwordsService = inject(PasswordsService); private platform = inject(Platform); private settingsService = inject(SettingsService); + private shareService = inject(ShareService); toast = inject(ToastController); private zone = inject(NgZone); @@ -41,6 +43,16 @@ export class HomePageComponent implements OnInit { constructor() { addIcons({ informationCircleOutline, warning, copy }); + + // React to shared text by populating the host field + effect(() => { + const sharedHost = this.shareService.sharedHost(); + if (sharedHost) { + this.input.host = sharedHost; + this.shareService.clearSharedHost(); + this.update(); + } + }); } async ngOnInit() { diff --git a/src/app/share.service.ts b/src/app/share.service.ts new file mode 100644 index 0000000..6d702ff --- /dev/null +++ b/src/app/share.service.ts @@ -0,0 +1,26 @@ +import { Injectable, signal } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class ShareService { + /** + * Signal holding the most recently shared text to use as the host field. + * Components can watch this and react when it changes. + */ + readonly sharedHost = signal(null); + + /** + * Set the shared host text. This will trigger any effects watching the signal. + */ + setSharedHost(host: string) { + this.sharedHost.set(host); + } + + /** + * Clear the shared host after it's been consumed. + */ + clearSharedHost() { + this.sharedHost.set(null); + } +} diff --git a/src/app/tabs/tabs.page.ts b/src/app/tabs/tabs.page.ts index 17f50b9..19d6dbc 100644 --- a/src/app/tabs/tabs.page.ts +++ b/src/app/tabs/tabs.page.ts @@ -1,7 +1,8 @@ -import { Component, OnInit, ViewChild, inject } from '@angular/core'; +import { Component, OnInit, ViewChild, effect, inject } from '@angular/core'; import { IonTabs, Platform } from '@ionic/angular/standalone'; import { addIcons } from 'ionicons'; import { key, settings } from 'ionicons/icons'; +import { ShareService } from '../share.service'; @Component({ selector: 'app-tabs', @@ -10,11 +11,20 @@ import { key, settings } from 'ionicons/icons'; }) export class TabsPageComponent implements OnInit { private platform = inject(Platform); + private shareService = inject(ShareService); @ViewChild('tabs') tab: IonTabs; constructor() { addIcons({ key, settings }); + + // Switch to home tab when a share is received + effect(() => { + const sharedHost = this.shareService.sharedHost(); + if (sharedHost) { + this.tab?.select('home'); + } + }); } ngOnInit() {