diff --git a/.gitignore b/.gitignore index ab644d6..830364a 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,7 @@ app.*.map.json # Android # ----------------------------- android/.gradle/ +android/local.properties android/app/debug/ android/app/profile/ android/app/release/ @@ -49,8 +50,15 @@ android/app/release/ # ----------------------------- ios/Flutter/ephemeral/ ios/Flutter/.last_build_id +ios/Flutter/Flutter.podspec +ios/Flutter/Generated.xcconfig +ios/Flutter/flutter_export_environment.sh +ios/Flutter/App.framework/ +ios/Flutter/Flutter.framework/ ios/Pods/ ios/.symlinks/ +ios/Runner/GeneratedPluginRegistrant.h +ios/Runner/GeneratedPluginRegistrant.m ios/Runner.xcworkspace/xcuserdata/ ios/Runner.xcodeproj/xcuserdata/ diff --git a/docs/ADR/0003-backendless-local-llm-pipeline.md b/docs/ADR/0003-backendless-local-llm-pipeline.md new file mode 100644 index 0000000..7065a70 --- /dev/null +++ b/docs/ADR/0003-backendless-local-llm-pipeline.md @@ -0,0 +1,64 @@ +# ADR 0003: Backendless Local LLM Pipeline + +## Status + +Accepted + +## Context + +The product goal is a phone-first relationship memory app. The user shares +messages and text snippets from messaging apps, resolves uncertain identity +matches over time, and receives private extraction plus useful suggestions. + +The app is moving away from a server-backed source of truth. Backend sync and +the fake backend should not create or overwrite relationship data unless a +developer explicitly enables REST sync for transport work. + +iOS does not provide a reliable always-running background service. Background +work must be treated as opportunistic: run on foreground/app resume, after share +intake, through user-initiated actions, and through scheduled background windows +when the OS grants time. + +## Decision + +Use a backendless local-first pipeline: + +1. Share intake stores raw shared payloads locally. +2. Strong identity matches attach directly to an existing person. +3. Ambiguous, conflicting, or low-information shares stay in `Share Inbox`. +4. Nightly extraction processes only new/resolved share batches. +5. Weekly/on-demand recommendations use compact profile summaries plus prior + suggestion history to avoid repeats. +6. Internet-grounded recommendations are generated only during the weekly or + manual recommendation phase, not during nightly extraction. +7. Fake backend sync is inert by default and must not seed local relationship + records. + +## LLM Cost Controls + +- Batch new shares into one extraction request per run. +- Use source fingerprints and content hashes to skip repeated extraction. +- Store extraction run fingerprints, completed timestamps, and failures. +- Send compact person tokens and normalized facts instead of names and raw + history when possible. +- Keep grounded shopping/event prompts separate from nightly extraction because + web search is more expensive and time-sensitive. +- Include accepted/dismissed/pending suggestion fingerprints in weekly prompts + so repeated concerts, shops, or gifts are suppressed. + +## iOS Background Policy + +- Treat scheduled nightly and weekly work as best-effort. +- Always offer manual `Run Now` actions. +- Prefer local notifications after work completes. +- Do not require APNs or a server daemon for core behavior. +- Keep work units short enough to survive iOS background expiration. + +## Consequences + +- The app remains useful without any backend. +- Share capture and profile building are durable across weeks/months of gradual + user review. +- Suggestions may run later than the configured wall-clock time on iOS. +- REST sync can still exist as a developer/integration path, but it is not part + of the primary product loop. diff --git a/docs/SETUP.md b/docs/SETUP.md index bdb3337..c9087e8 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -25,7 +25,8 @@ flutter run --dart-define=USE_FAKE_BACKEND=true ## Background Sync Background sync (startup + resume + periodic ticks while authenticated) is -enabled by default. +disabled by default. The current product direction is backendless/local-first, +so backend sync should be enabled only for REST transport development. In REST mode, auto-triggers are reachability-gated to avoid unnecessary sync attempts while offline. @@ -78,7 +79,23 @@ Behavior notes: ## Phone-Only Private AI Digest The app can run without a production backend by keeping `USE_FAKE_BACKEND=true` -and using the LLM integration only for a private weekly digest. +and using the LLM integration for local profile extraction and private digest +generation. + +Selected product flow: + +- share messages/text from WhatsApp, iMessage, Notes, Safari, or similar apps + into the app +- auto-match only when identity evidence is strong +- keep ambiguous or low-information shares in `Share Inbox` +- let the user gradually resolve inbox items over days, weeks, and months +- run local/profile-building extraction opportunistically and on scheduled + nightly windows when the OS allows it +- run grounded weekly recommendations on request or on the configured digest + schedule + +Backend sync is not part of this path. The fake backend is intentionally inert +for sync pulls and must not seed people or other relationship records. Digest behavior: @@ -86,6 +103,10 @@ Digest behavior: - scheduled digest payloads use pseudonymous tokens such as `person_001` - names, aliases, sender names, source URLs, raw shared text, and sensitive notes are not sent in the scheduled digest prompt +- repeated LLM work should be avoided by fingerprinting unresolved/new share + batches and skipping extraction when the same batch has already completed +- weekly recommendation prompts should include previously suggested/dismissed + items so the model avoids repeats - LLM results are saved as pending AI review drafts first - accepting a draft creates a local idea, task, or reminder - dismissing a draft leaves existing local data unchanged diff --git a/ios/Podfile b/ios/Podfile index bde92ce..04c36cf 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -32,9 +32,6 @@ target 'Runner' do use_modular_headers! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) - target 'Share Extension' do - inherit! :search_paths - end target 'RunnerTests' do inherit! :search_paths end diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 10ae44d..63ab736 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -75,6 +75,6 @@ SPEC CHECKSUMS: shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb workmanager_apple: 904529ae31e97fc5be632cf628507652294a0778 -PODFILE CHECKSUM: 09dd68ff20d1f7e1497be5a3d807622c3053984a +PODFILE CHECKSUM: 1959d098c91d8a792531a723c4a9d7e9f6a01e38 COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 09f54f2..3361173 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -9,10 +9,6 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 15BE7299C8BED38E2BC0A6A0 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C88FBC0E3BAB70074730065 /* Pods_Runner.framework */; }; - A1B2C3011111111111111111 /* Pods_Share_Extension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A1B2C30B1111111111111111 /* Pods_Share_Extension.framework */; }; - A1B2C3021111111111111111 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C30D1111111111111111 /* ShareViewController.swift */; }; - A1B2C3031111111111111111 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3181111111111111111 /* MainInterface.storyboard */; }; - A1B2C3041111111111111111 /* Share Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = A1B2C30C1111111111111111 /* Share Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 5E1E9B720DF095CAC1A2FF8A /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD3F9117486EC23DF50E2D6A /* Pods_RunnerTests.framework */; }; @@ -21,16 +17,12 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + A1B2C3021111111111111111 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C30D1111111111111111 /* ShareViewController.swift */; }; + A1B2C3031111111111111111 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3181111111111111111 /* MainInterface.storyboard */; }; + A1B2C3041111111111111111 /* Share Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = A1B2C30C1111111111111111 /* Share Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - A1B2C3051111111111111111 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 97C146E61CF9000F007C117D /* Project object */; - proxyType = 1; - remoteGlobalIDString = A1B2C3131111111111111111; - remoteInfo = "Share Extension"; - }; 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 97C146E61CF9000F007C117D /* Project object */; @@ -38,9 +30,26 @@ remoteGlobalIDString = 97C146ED1CF9000F007C117D; remoteInfo = Runner; }; + A1B2C3051111111111111111 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = A1B2C3131111111111111111; + remoteInfo = "Share Extension"; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; A1B2C3061111111111111111 /* Embed App Extensions */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -52,32 +61,12 @@ name = "Embed App Extensions"; runOnlyForDeploymentPostprocessing = 0; }; - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 0E770A251CA9930160492DC4 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - A1B2C3071111111111111111 /* Pods-Share Extension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Share Extension.debug.xcconfig"; path = "Target Support Files/Pods-Share Extension/Pods-Share Extension.debug.xcconfig"; sourceTree = ""; }; - A1B2C3081111111111111111 /* Pods-Share Extension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Share Extension.release.xcconfig"; path = "Target Support Files/Pods-Share Extension/Pods-Share Extension.release.xcconfig"; sourceTree = ""; }; - A1B2C3091111111111111111 /* Pods-Share Extension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Share Extension.profile.xcconfig"; path = "Target Support Files/Pods-Share Extension/Pods-Share Extension.profile.xcconfig"; sourceTree = ""; }; - A1B2C30A1111111111111111 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; - A1B2C30B1111111111111111 /* Pods_Share_Extension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Share_Extension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - A1B2C30C1111111111111111 /* Share Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Share Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; - A1B2C30D1111111111111111 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; - A1B2C30E1111111111111111 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; - A1B2C30F1111111111111111 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - A1B2C3101111111111111111 /* Share Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Share Extension.entitlements"; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; @@ -96,20 +85,18 @@ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9B7CFEE2CE4B7B1DCB85B3E1 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + A1B2C30A1111111111111111 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; + A1B2C30C1111111111111111 /* Share Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Share Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; + A1B2C30D1111111111111111 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; + A1B2C30E1111111111111111 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; + A1B2C30F1111111111111111 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A1B2C3101111111111111111 /* Share Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Share Extension.entitlements"; sourceTree = ""; }; CD3F9117486EC23DF50E2D6A /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; E9B506D809ABF2A50BF6135B /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; F2C7EE505B336533B62B23EB /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - A1B2C3111111111111111111 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - A1B2C3011111111111111111 /* Pods_Share_Extension.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -118,6 +105,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + A1B2C3111111111111111111 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; E2BAEB33085D4BFE88A55D45 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -143,14 +137,10 @@ E9B506D809ABF2A50BF6135B /* Pods-Runner.debug.xcconfig */, 9B7CFEE2CE4B7B1DCB85B3E1 /* Pods-Runner.release.xcconfig */, 0E770A251CA9930160492DC4 /* Pods-Runner.profile.xcconfig */, - A1B2C3071111111111111111 /* Pods-Share Extension.debug.xcconfig */, - A1B2C3081111111111111111 /* Pods-Share Extension.release.xcconfig */, - A1B2C3091111111111111111 /* Pods-Share Extension.profile.xcconfig */, F2C7EE505B336533B62B23EB /* Pods-RunnerTests.debug.xcconfig */, 9708213885153303B111D6D9 /* Pods-RunnerTests.release.xcconfig */, 6881917E7D40106AEFE87622 /* Pods-RunnerTests.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -158,7 +148,6 @@ isa = PBXGroup; children = ( 4C88FBC0E3BAB70074730065 /* Pods_Runner.framework */, - A1B2C30B1111111111111111 /* Pods_Share_Extension.framework */, CD3F9117486EC23DF50E2D6A /* Pods_RunnerTests.framework */, ); name = Frameworks; @@ -229,24 +218,6 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - A1B2C3131111111111111111 /* Share Extension */ = { - isa = PBXNativeTarget; - buildConfigurationList = A1B2C31C1111111111111111 /* Build configuration list for PBXNativeTarget "Share Extension" */; - buildPhases = ( - A1B2C3151111111111111111 /* [CP] Check Pods Manifest.lock */, - A1B2C3161111111111111111 /* Sources */, - A1B2C3111111111111111111 /* Frameworks */, - A1B2C3141111111111111111 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = "Share Extension"; - productName = "Share Extension"; - productReference = A1B2C30C1111111111111111 /* Share Extension.appex */; - productType = "com.apple.product-type.app-extension"; - }; 331C8080294A63A400263BE5 /* RunnerTests */ = { isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; @@ -290,6 +261,23 @@ productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; + A1B2C3131111111111111111 /* Share Extension */ = { + isa = PBXNativeTarget; + buildConfigurationList = A1B2C31C1111111111111111 /* Build configuration list for PBXNativeTarget "Share Extension" */; + buildPhases = ( + A1B2C3161111111111111111 /* Sources */, + A1B2C3111111111111111111 /* Frameworks */, + A1B2C3141111111111111111 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Share Extension"; + productName = "Share Extension"; + productReference = A1B2C30C1111111111111111 /* Share Extension.appex */; + productType = "com.apple.product-type.app-extension"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -300,9 +288,6 @@ LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { - A1B2C3131111111111111111 = { - CreatedOnToolsVersion = 15.0.1; - }; 331C8080294A63A400263BE5 = { CreatedOnToolsVersion = 14.0; TestTargetID = 97C146ED1CF9000F007C117D; @@ -311,6 +296,9 @@ CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1100; }; + A1B2C3131111111111111111 = { + CreatedOnToolsVersion = 15.0.1; + }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; @@ -334,14 +322,6 @@ /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - A1B2C3141111111111111111 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - A1B2C3031111111111111111 /* MainInterface.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 331C807F294A63A400263BE5 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -360,31 +340,17 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + A1B2C3141111111111111111 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A1B2C3031111111111111111 /* MainInterface.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - A1B2C3151111111111111111 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Share Extension-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; 38116227E31B7524BBB62C35 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -480,14 +446,6 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - A1B2C3161111111111111111 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - A1B2C3021111111111111111 /* ShareViewController.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 331C807D294A63A400263BE5 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -506,30 +464,30 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + A1B2C3161111111111111111 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A1B2C3021111111111111111 /* ShareViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - A1B2C3171111111111111111 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = A1B2C3131111111111111111 /* Share Extension */; - targetProxy = A1B2C3051111111111111111 /* PBXContainerItemProxy */; - }; 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 97C146ED1CF9000F007C117D /* Runner */; targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; }; + A1B2C3171111111111111111 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = A1B2C3131111111111111111 /* Share Extension */; + targetProxy = A1B2C3051111111111111111 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ - A1B2C3181111111111111111 /* MainInterface.storyboard */ = { - isa = PBXVariantGroup; - children = ( - A1B2C30E1111111111111111 /* Base */, - ); - name = MainInterface.storyboard; - sourceTree = ""; - }; 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -546,18 +504,26 @@ name = LaunchScreen.storyboard; sourceTree = ""; }; + A1B2C3181111111111111111 /* MainInterface.storyboard */ = { + isa = PBXVariantGroup; + children = ( + A1B2C30E1111111111111111 /* Base */, + ); + name = MainInterface.storyboard; + sourceTree = ""; + }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ - 249021D3217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - APP_BUNDLE_IDENTIFIER = com.example.relationshipSaver; - APP_GROUP_ID = "group.$(APP_BUNDLE_IDENTIFIER).shared"; - APP_RUNNER_TESTS_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).RunnerTests"; - APP_SHARE_EXTENSION_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).ShareExtension"; - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + APP_BUNDLE_IDENTIFIER = com.rijadzuzo.relationshipSaver; + APP_GROUP_ID = group.com.rijadzuzo.relationshipSaver.shared; + APP_RUNNER_TESTS_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).RunnerTests"; + APP_SHARE_EXTENSION_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).ShareExtension"; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -584,10 +550,10 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM_ID = ""; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM_ID = ""; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; @@ -608,122 +574,40 @@ }; 249021D4217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - CUSTOM_GROUP_ID = "$(APP_GROUP_ID)"; - DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM_ID)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CURRENT_PROJECT_VERSION = 1; + CUSTOM_GROUP_ID = "$(APP_GROUP_ID)"; + DEVELOPMENT_TEAM = SR3UKQVKP6; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; }; - A1B2C3191111111111111111 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = A1B2C3071111111111111111 /* Pods-Share Extension.debug.xcconfig */; - buildSettings = { - CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - CUSTOM_GROUP_ID = "$(APP_GROUP_ID)"; - DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM_ID)"; - INFOPLIST_FILE = "Share Extension/Info.plist"; - INFOPLIST_KEY_CFBundleDisplayName = "Relationship Saver Share"; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@executable_path/../../Frameworks", - ); - MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)"; - PRODUCT_BUNDLE_IDENTIFIER = "$(APP_SHARE_EXTENSION_BUNDLE_IDENTIFIER)"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - A1B2C31A1111111111111111 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = A1B2C3081111111111111111 /* Pods-Share Extension.release.xcconfig */; - buildSettings = { - CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - CUSTOM_GROUP_ID = "$(APP_GROUP_ID)"; - DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM_ID)"; - INFOPLIST_FILE = "Share Extension/Info.plist"; - INFOPLIST_KEY_CFBundleDisplayName = "Relationship Saver Share"; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@executable_path/../../Frameworks", - ); - MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)"; - PRODUCT_BUNDLE_IDENTIFIER = "$(APP_SHARE_EXTENSION_BUNDLE_IDENTIFIER)"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - A1B2C31B1111111111111111 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = A1B2C3091111111111111111 /* Pods-Share Extension.profile.xcconfig */; - buildSettings = { - CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - CUSTOM_GROUP_ID = "$(APP_GROUP_ID)"; - DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM_ID)"; - INFOPLIST_FILE = "Share Extension/Info.plist"; - INFOPLIST_KEY_CFBundleDisplayName = "Relationship Saver Share"; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@executable_path/../../Frameworks", - ); - MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)"; - PRODUCT_BUNDLE_IDENTIFIER = "$(APP_SHARE_EXTENSION_BUNDLE_IDENTIFIER)"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Profile; - }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = F2C7EE505B336533B62B23EB /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "$(APP_RUNNER_TESTS_BUNDLE_IDENTIFIER)"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "$(APP_RUNNER_TESTS_BUNDLE_IDENTIFIER)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; @@ -733,15 +617,15 @@ 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9708213885153303B111D6D9 /* Pods-RunnerTests.release.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "$(APP_RUNNER_TESTS_BUNDLE_IDENTIFIER)"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "$(APP_RUNNER_TESTS_BUNDLE_IDENTIFIER)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; }; name = Release; @@ -750,27 +634,27 @@ isa = XCBuildConfiguration; baseConfigurationReference = 6881917E7D40106AEFE87622 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "$(APP_RUNNER_TESTS_BUNDLE_IDENTIFIER)"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "$(APP_RUNNER_TESTS_BUNDLE_IDENTIFIER)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; }; name = Profile; }; - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - APP_BUNDLE_IDENTIFIER = com.example.relationshipSaver; - APP_GROUP_ID = "group.$(APP_BUNDLE_IDENTIFIER).shared"; - APP_RUNNER_TESTS_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).RunnerTests"; - APP_SHARE_EXTENSION_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).ShareExtension"; - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + APP_BUNDLE_IDENTIFIER = com.rijadzuzo.relationshipSaver; + APP_GROUP_ID = group.com.rijadzuzo.relationshipSaver.shared; + APP_RUNNER_TESTS_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).RunnerTests"; + APP_SHARE_EXTENSION_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).ShareExtension"; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -797,10 +681,10 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM_ID = ""; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM_ID = ""; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; @@ -824,15 +708,15 @@ }; name = Debug; }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - APP_BUNDLE_IDENTIFIER = com.example.relationshipSaver; - APP_GROUP_ID = "group.$(APP_BUNDLE_IDENTIFIER).shared"; - APP_RUNNER_TESTS_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).RunnerTests"; - APP_SHARE_EXTENSION_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).ShareExtension"; - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + APP_BUNDLE_IDENTIFIER = com.rijadzuzo.relationshipSaver; + APP_GROUP_ID = group.com.rijadzuzo.relationshipSaver.shared; + APP_RUNNER_TESTS_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).RunnerTests"; + APP_SHARE_EXTENSION_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).ShareExtension"; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -859,10 +743,10 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM_ID = ""; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM_ID = ""; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; @@ -885,23 +769,23 @@ }; 97C147061CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - CUSTOM_GROUP_ID = "$(APP_GROUP_ID)"; - DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM_ID)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + CUSTOM_GROUP_ID = "$(APP_GROUP_ID)"; + DEVELOPMENT_TEAM = SR3UKQVKP6; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -910,41 +794,110 @@ }; 97C147071CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - CUSTOM_GROUP_ID = "$(APP_GROUP_ID)"; - DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM_ID)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + CUSTOM_GROUP_ID = "$(APP_GROUP_ID)"; + DEVELOPMENT_TEAM = SR3UKQVKP6; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; + A1B2C3191111111111111111 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + CUSTOM_GROUP_ID = "$(APP_GROUP_ID)"; + DEVELOPMENT_TEAM = SR3UKQVKP6; + INFOPLIST_FILE = "Share Extension/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "Relationship Saver"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "$(APP_SHARE_EXTENSION_BUNDLE_IDENTIFIER)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + A1B2C31A1111111111111111 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + CUSTOM_GROUP_ID = "$(APP_GROUP_ID)"; + DEVELOPMENT_TEAM = SR3UKQVKP6; + INFOPLIST_FILE = "Share Extension/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "Relationship Saver"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "$(APP_SHARE_EXTENSION_BUNDLE_IDENTIFIER)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + A1B2C31B1111111111111111 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + CUSTOM_GROUP_ID = "$(APP_GROUP_ID)"; + DEVELOPMENT_TEAM = SR3UKQVKP6; + INFOPLIST_FILE = "Share Extension/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "Relationship Saver"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "$(APP_SHARE_EXTENSION_BUNDLE_IDENTIFIER)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Profile; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - A1B2C31C1111111111111111 /* Build configuration list for PBXNativeTarget "Share Extension" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - A1B2C3191111111111111111 /* Debug */, - A1B2C31A1111111111111111 /* Release */, - A1B2C31B1111111111111111 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -975,6 +928,16 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + A1B2C31C1111111111111111 /* Build configuration list for PBXNativeTarget "Share Extension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A1B2C3191111111111111111 /* Debug */, + A1B2C31A1111111111111111 /* Release */, + A1B2C31B1111111111111111 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index e3773d4..52bde3b 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -52,9 +52,9 @@ + AppGroupId + $(CUSTOM_GROUP_ID) + BGTaskSchedulerPermittedIdentifiers + + com.relationshipsaver.llm.digest + CADisableMinimumFrameDurationOnPhone CFBundleDevelopmentRegion @@ -12,8 +18,6 @@ $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) - AppGroupId - $(CUSTOM_GROUP_ID) CFBundleInfoDictionaryVersion 6.0 CFBundleName @@ -24,8 +28,6 @@ $(FLUTTER_BUILD_NAME) CFBundleSignature ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) CFBundleURLTypes @@ -37,6 +39,8 @@ + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS NSPhotoLibraryUsageDescription @@ -69,10 +73,6 @@ fetch processing - BGTaskSchedulerPermittedIdentifiers - - com.relationshipsaver.llm.digest - UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/ios/Share Extension/Info.plist b/ios/Share Extension/Info.plist index 0fd3c40..ddd1f6c 100644 --- a/ios/Share Extension/Info.plist +++ b/ios/Share Extension/Info.plist @@ -4,30 +4,44 @@ AppGroupId $(CUSTOM_GROUP_ID) + CFBundleDisplayName + Relationship Saver + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + $(MARKETING_VERSION) CFBundleVersion - $(FLUTTER_BUILD_NUMBER) + $(CURRENT_PROJECT_VERSION) NSExtension NSExtensionAttributes + NSExtensionActivationRule + + NSExtensionActivationSupportsFileWithMaxCount + 5 + NSExtensionActivationSupportsImageWithMaxCount + 20 + NSExtensionActivationSupportsMovieWithMaxCount + 10 + NSExtensionActivationSupportsText + + NSExtensionActivationSupportsWebURLWithMaxCount + 1 + PHSupportedMediaTypes Video Image - NSExtensionActivationRule - - NSExtensionActivationSupportsText - - NSExtensionActivationSupportsWebURLWithMaxCount - 1 - NSExtensionActivationSupportsImageWithMaxCount - 20 - NSExtensionActivationSupportsMovieWithMaxCount - 10 - NSExtensionActivationSupportsFileWithMaxCount - 5 - NSExtensionMainStoryboard MainInterface diff --git a/ios/Share Extension/ShareViewController.swift b/ios/Share Extension/ShareViewController.swift index d70fe91..49f383a 100644 --- a/ios/Share Extension/ShareViewController.swift +++ b/ios/Share Extension/ShareViewController.swift @@ -1,3 +1,315 @@ -import receive_sharing_intent +import MobileCoreServices +import Social +import UniformTypeIdentifiers +import UIKit -class ShareViewController: RSIShareViewController {} +private let schemePrefix = "ShareMedia" +private let userDefaultsKey = "ShareKey" +private let userDefaultsMessageKey = "ShareMessageKey" +private let appGroupIdKey = "AppGroupId" + +private struct SharedMediaFile: Codable { + let path: String + let mimeType: String? + let thumbnail: String? + let duration: Double? + let message: String? + let type: SharedMediaType +} + +private enum SharedMediaType: String, Codable, CaseIterable { + case image + case video + case text + case file + case url + + var typeIdentifier: String { + if #available(iOS 14.0, *) { + switch self { + case .image: + return UTType.image.identifier + case .video: + return UTType.movie.identifier + case .text: + return UTType.text.identifier + case .file: + return UTType.fileURL.identifier + case .url: + return UTType.url.identifier + } + } + + switch self { + case .image: + return kUTTypeImage as String + case .video: + return kUTTypeMovie as String + case .text: + return kUTTypeText as String + case .file: + return kUTTypeFileURL as String + case .url: + return kUTTypeURL as String + } + } +} + +final class ShareViewController: SLComposeServiceViewController { + private var hostAppBundleIdentifier = "" + private var appGroupId = "" + private var sharedMedia: [SharedMediaFile] = [] + private let group = DispatchGroup() + private var didFinishLoadingAttachments = false + + override func viewDidLoad() { + super.viewDidLoad() + loadIdentifiers() + } + + override func isContentValid() -> Bool { + true + } + + override func didSelectPost() { + appendContentTextIfNeeded() + saveAndRedirect(message: contentText) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + guard !didFinishLoadingAttachments else { + return + } + didFinishLoadingAttachments = true + loadAttachments() + } + + override func configurationItems() -> [Any]! { + [] + } + + private func loadIdentifiers() { + let shareExtensionIdentifier = Bundle.main.bundleIdentifier ?? "" + if let lastDotIndex = shareExtensionIdentifier.lastIndex(of: ".") { + hostAppBundleIdentifier = String(shareExtensionIdentifier[.. nextPeople = current.people.toList( - growable: true, - ); if (resolvedPerson == null) { - final String? inferredName = _deriveAutoProfileName( - sourceDisplayName: sourceDisplayName, - sourceUserId: sourceUserId, - sourceThreadId: sourceThreadId, + return _queueSharedInbox( + current: current, + payload: payload, + sourceFingerprint: sourceFingerprint, + normalizedDisplayName: normalizedDisplayName, + reason: SharedInboxReason.missingIdentity, + candidateProfileIds: const [], ); - if (inferredName == null) { - return _queueSharedInbox( - current: current, - payload: payload, - sourceFingerprint: sourceFingerprint, - normalizedDisplayName: normalizedDisplayName, - reason: SharedInboxReason.missingIdentity, - candidateProfileIds: const [], - ); - } - createdProfile = true; - resolvedPerson = PersonProfile( - id: 'p-${_uuid.v4()}', - name: inferredName, - relationship: 'WhatsApp Contact', - affinityScore: 70, - nextMoment: now.add(const Duration(days: 2)), - tags: const ['whatsapp'], - notes: 'Auto-created from shared message.', - aliases: sourceDisplayName == null || sourceDisplayName == inferredName - ? const [] - : [sourceDisplayName], - lastUpdatedAt: now, - ); - nextPeople.insert(0, resolvedPerson); } return _ingestIntoResolvedProfile( @@ -141,8 +115,8 @@ extension LocalRepositoryShareOperations on LocalRepository { normalizedDisplayName: normalizedDisplayName, resolvedPerson: resolvedPerson, matchedLink: matchedLink, - createdProfile: createdProfile, - people: nextPeople, + createdProfile: false, + people: current.people, resolvedAutomatically: true, draft: _defaultDraftForPayload(payload), ); @@ -298,14 +272,10 @@ extension LocalRepositoryShareOperations on LocalRepository { }) async { final LocalDataState current = _requireState(); final DateTime now = DateTime.now(); - final String profileName = - _trimToNull(name) ?? - _deriveAutoProfileName( - sourceDisplayName: payload.sourceDisplayName, - sourceUserId: payload.sourceUserId, - sourceThreadId: payload.sourceThreadId, - ) ?? - 'New Contact'; + final String? profileName = _trimToNull(name); + if (profileName == null) { + throw ArgumentError('A person name is required to create from a share'); + } final String normalizedRelationship = relationship.trim().isEmpty ? 'Contact' : relationship.trim(); @@ -842,16 +812,6 @@ SharedInboxEntry _requireSharedInboxEntry( throw ArgumentError('Shared inbox entry not found'); } -String? _deriveAutoProfileName({ - required String? sourceDisplayName, - required String? sourceUserId, - required String? sourceThreadId, -}) { - return _trimToNull(sourceDisplayName) ?? - _trimToNull(sourceUserId) ?? - _trimToNull(sourceThreadId); -} - String? _buildStableSourceFingerprint({ required String sourceApp, required String? sourceUserId, diff --git a/lib/app/navigation/app_destination.dart b/lib/app/navigation/app_destination.dart index bd77b8d..f1bd972 100644 --- a/lib/app/navigation/app_destination.dart +++ b/lib/app/navigation/app_destination.dart @@ -2,14 +2,9 @@ import 'package:flutter/material.dart'; /// Typed shell destinations used by the app navigation shell. enum AppDestination { - dashboard('Dashboard', Icons.dashboard_rounded), + shareInbox('Capture', Icons.inbox_rounded), people('People', Icons.people_alt_rounded), - moments('Moments', Icons.auto_awesome_rounded), - signals('Signals', Icons.tips_and_updates_rounded), - aiReview('AI Review', Icons.rate_review_rounded), - ideas('Ideas', Icons.lightbulb_outline_rounded), - reminders('Reminders', Icons.notifications_active_outlined), - sync('Sync', Icons.sync_rounded), + aiReview('Digest', Icons.rate_review_rounded), settings('Settings', Icons.settings_rounded); const AppDestination(this.label, this.icon); @@ -17,6 +12,3 @@ enum AppDestination { final String label; final IconData icon; } - -/// Secondary destinations exposed from the compact shell overflow menu. -enum CompactSecondaryDestination { aiReview, ideas, reminders, sync, settings } diff --git a/lib/app/presentation/app_shell.dart b/lib/app/presentation/app_shell.dart index cb10b60..06ccf11 100644 --- a/lib/app/presentation/app_shell.dart +++ b/lib/app/presentation/app_shell.dart @@ -3,14 +3,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:relationship_saver/app/navigation/app_destination.dart'; import 'package:relationship_saver/core/config/app_theme.dart'; import 'package:relationship_saver/features/ai_digest/presentation/ai_digest_review_view.dart'; -import 'package:relationship_saver/features/dashboard/presentation/dashboard_view.dart'; -import 'package:relationship_saver/features/ideas/presentation/ideas_view.dart'; -import 'package:relationship_saver/features/moments/presentation/moments_view.dart'; import 'package:relationship_saver/features/people/presentation/people_view.dart'; -import 'package:relationship_saver/features/reminders/presentation/reminders_view.dart'; import 'package:relationship_saver/features/settings/presentation/settings_view.dart'; -import 'package:relationship_saver/features/signals/presentation/signals_view.dart'; -import 'package:relationship_saver/features/sync/presentation/sync_view.dart'; +import 'package:relationship_saver/features/share_intake/presentation/share_inbox_view.dart'; /// Responsive shell that hosts the main feature slices. class AppShell extends ConsumerStatefulWidget { @@ -21,16 +16,11 @@ class AppShell extends ConsumerStatefulWidget { } class _AppShellState extends ConsumerState { - AppDestination _destination = AppDestination.dashboard; + AppDestination _destination = AppDestination.shareInbox; static const List _destinations = AppDestination.values; static const List _compactPrimaryDestinations = - [ - AppDestination.dashboard, - AppDestination.people, - AppDestination.moments, - AppDestination.signals, - ]; + AppDestination.values; @override Widget build(BuildContext context) { @@ -65,6 +55,7 @@ class _AppShellState extends ConsumerState { Expanded( child: AnimatedSwitcher( duration: const Duration(milliseconds: 250), + layoutBuilder: _expandedSwitcherLayout, child: KeyedSubtree( key: ValueKey(_destination), child: _screenFor(_destination), @@ -84,31 +75,10 @@ class _AppShellState extends ConsumerState { final int compactIndex = _compactSelectedIndex(_destination); final AppDestination active = _compactPrimaryDestinations[compactIndex]; return _CompactShell( - title: active.label, selected: active, body: _screenFor(active), destinations: _compactPrimaryDestinations, onChange: _selectDestination, - onOpenSecondary: (CompactSecondaryDestination destination) { - final AppDestination screenDestination = switch (destination) { - CompactSecondaryDestination.ideas => AppDestination.ideas, - CompactSecondaryDestination.aiReview => AppDestination.aiReview, - CompactSecondaryDestination.reminders => AppDestination.reminders, - CompactSecondaryDestination.sync => AppDestination.sync, - CompactSecondaryDestination.settings => AppDestination.settings, - }; - - Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) { - return _CompactSecondaryScreen( - title: screenDestination.label, - child: _screenFor(screenDestination), - ); - }, - ), - ); - }, ); } @@ -129,22 +99,12 @@ class _AppShellState extends ConsumerState { Widget _screenFor(AppDestination destination) { switch (destination) { - case AppDestination.dashboard: - return const DashboardView(); + case AppDestination.shareInbox: + return const ShareInboxView(); case AppDestination.people: return const PeopleView(); - case AppDestination.moments: - return const MomentsView(); - case AppDestination.signals: - return const SignalsView(); case AppDestination.aiReview: return const AiDigestReviewView(); - case AppDestination.ideas: - return const IdeasView(); - case AppDestination.reminders: - return const RemindersView(); - case AppDestination.sync: - return const SyncView(); case AppDestination.settings: return const SettingsView(); } @@ -255,7 +215,7 @@ class _Sidebar extends StatelessWidget { borderRadius: BorderRadius.circular(16), ), child: Text( - 'Tip: Use Signals daily and convert at least one into a concrete plan.', + 'Share messages or paste text into Capture. Nothing is assigned until you choose a person.', style: Theme.of( context, ).textTheme.bodyMedium?.copyWith(color: AppTheme.textSecondary), @@ -269,69 +229,25 @@ class _Sidebar extends StatelessWidget { class _CompactShell extends StatelessWidget { const _CompactShell({ - required this.title, required this.selected, required this.body, required this.destinations, required this.onChange, - required this.onOpenSecondary, }); - final String title; final AppDestination selected; final Widget body; final List destinations; final ValueChanged onChange; - final ValueChanged onOpenSecondary; @override Widget build(BuildContext context) { return Column( children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), - child: Row( - children: [ - Expanded( - child: Text( - title, - style: Theme.of(context).textTheme.titleLarge, - ), - ), - PopupMenuButton( - tooltip: 'More', - icon: const Icon(Icons.more_horiz_rounded), - onSelected: onOpenSecondary, - itemBuilder: (BuildContext context) => - >[ - const PopupMenuItem( - value: CompactSecondaryDestination.aiReview, - child: Text('AI Review'), - ), - const PopupMenuItem( - value: CompactSecondaryDestination.ideas, - child: Text('Ideas'), - ), - const PopupMenuItem( - value: CompactSecondaryDestination.reminders, - child: Text('Reminders'), - ), - const PopupMenuItem( - value: CompactSecondaryDestination.sync, - child: Text('Sync'), - ), - const PopupMenuItem( - value: CompactSecondaryDestination.settings, - child: Text('Settings'), - ), - ], - ), - ], - ), - ), Expanded( child: AnimatedSwitcher( duration: const Duration(milliseconds: 220), + layoutBuilder: _expandedSwitcherLayout, child: KeyedSubtree( key: ValueKey(selected), child: body, @@ -355,27 +271,12 @@ class _CompactShell extends StatelessWidget { } } -class _CompactSecondaryScreen extends StatelessWidget { - const _CompactSecondaryScreen({required this.title, required this.child}); - - final String title; - final Widget child; - - @override - Widget build(BuildContext context) { - return DecoratedBox( - decoration: const BoxDecoration( - gradient: LinearGradient( - colors: [Color(0xFFF3F8FB), Color(0xFFEAF1F8)], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - ), - child: Scaffold( - backgroundColor: Colors.transparent, - appBar: AppBar(title: Text(title)), - body: child, - ), - ); - } +Widget _expandedSwitcherLayout( + Widget? currentChild, + List previousChildren, +) { + return Stack( + fit: StackFit.expand, + children: [...previousChildren, ?currentChild], + ); } diff --git a/lib/app/state/local_data_state.dart b/lib/app/state/local_data_state.dart index 2ef1c6d..213fa7e 100644 --- a/lib/app/state/local_data_state.dart +++ b/lib/app/state/local_data_state.dart @@ -219,162 +219,15 @@ class LocalDataState { ); } - static LocalDataState seed() { - return LocalDataState( - people: [ - PersonProfile( - id: 'p-ava', - name: 'Ava Hart', - relationship: 'Partner', - affinityScore: 92, - nextMoment: DateTime(2026, 2, 16, 19, 30), - tags: ['coffee', 'books', 'slow mornings'], - notes: 'Prefers thoughtful plans over expensive plans.', - aliases: ['Aves'], - location: 'Austin, TX', - lastUpdatedAt: DateTime(2026, 2, 14, 9, 0), - lastInteractedAt: DateTime(2026, 2, 12, 18, 30), - ), - PersonProfile( - id: 'p-jordan', - name: 'Jordan Lee', - relationship: 'Close Friend', - affinityScore: 78, - nextMoment: DateTime(2026, 2, 18, 18, 0), - tags: ['running', 'street food'], - notes: 'Has a race on Sunday; ask how training is going.', - location: 'Chicago, IL', - lastUpdatedAt: DateTime(2026, 2, 13, 8, 30), - lastInteractedAt: DateTime(2026, 2, 11, 19, 0), - ), - PersonProfile( - id: 'p-mila', - name: 'Mila Stone', - relationship: 'Sister', - affinityScore: 84, - nextMoment: DateTime(2026, 2, 20, 12, 0), - tags: ['plants', 'retro music'], - notes: 'Birthday prep should start this week.', - aliases: ['Millie'], - location: 'Seattle, WA', - lastUpdatedAt: DateTime(2026, 2, 13, 14, 20), - lastInteractedAt: DateTime(2026, 2, 12, 14, 20), - ), - ], - moments: [ - RelationshipMoment( - id: 'm-1', - personId: 'p-ava', - title: 'Sunday Walk Tradition', - summary: 'Short walk + no-phone hour worked really well.', - at: DateTime(2026, 2, 9, 9, 30), - type: 'ritual', - ), - RelationshipMoment( - id: 'm-2', - personId: 'p-jordan', - title: 'Post-workout Check-in', - summary: 'Shared training playlist and grabbed smoothies.', - at: DateTime(2026, 2, 11, 19, 0), - type: 'support', - ), - RelationshipMoment( - id: 'm-3', - personId: 'p-mila', - title: 'Gift Idea Captured', - summary: 'Found a vintage lamp from her saved style board.', - at: DateTime(2026, 2, 12, 14, 20), - type: 'gift', - ), - ], - ideas: [ - RelationshipIdea( - id: 'i-1', - personId: 'p-ava', - type: IdeaType.gift, - title: 'Handwritten recipe journal', - details: 'Collect 10 memories and recipes from this year.', - createdAt: DateTime(2026, 2, 12, 9), - ), - RelationshipIdea( - id: 'i-2', - personId: 'p-mila', - type: IdeaType.event, - title: 'Plant market + brunch date', - details: 'Saturday morning slot; book nearby cafe in advance.', - createdAt: DateTime(2026, 2, 13, 11), - ), - ], - reminders: [ - ReminderRule( - id: 'r-1', - personId: 'p-jordan', - title: 'Weekly check-in message', - cadence: ReminderCadence.weekly, - nextAt: DateTime(2026, 2, 18, 18), - ), - ReminderRule( - id: 'r-2', - personId: 'p-ava', - title: 'Sunday ritual planning', - cadence: ReminderCadence.weekly, - nextAt: DateTime(2026, 2, 16, 10), - ), - ], - tasks: [ - DashboardTask( - id: 't-1', - title: 'Plan Friday dinner with Ava', - description: 'Keep it low-key: ramen + bookstore stop.', - when: DateTime(2026, 2, 16, 17, 0), - ), - DashboardTask( - id: 't-2', - title: 'Send race-day encouragement to Jordan', - description: 'Voice note before 8:00 AM.', - when: DateTime(2026, 2, 17, 7, 30), - ), - DashboardTask( - id: 't-3', - title: 'Finalize Mila birthday shortlist', - description: 'Pick 1 meaningful gift and 1 backup.', - when: DateTime(2026, 2, 18, 20, 0), - ), - ], - personFacts: [ - PersonFact( - id: 'pf-1', - personId: 'p-ava', - type: CapturedFactType.like, - text: 'Quiet brunch spots and recipe-book stores', - label: 'Weekend preference', - sourceKind: CaptureSourceKind.manual, - createdAt: DateTime(2026, 2, 10, 8, 0), - updatedAt: DateTime(2026, 2, 10, 8, 0), - ), - PersonFact( - id: 'pf-2', - personId: 'p-mila', - type: CapturedFactType.giftIdea, - text: 'Vintage ceramic planter in muted green', - label: 'Gift lead', - sourceKind: CaptureSourceKind.manual, - createdAt: DateTime(2026, 2, 12, 14, 25), - updatedAt: DateTime(2026, 2, 12, 14, 25), - ), - ], - importantDates: [ - PersonImportantDate( - id: 'pd-1', - personId: 'p-mila', - label: 'Birthday', - date: DateTime(2026, 3, 3), - classification: 'birthday', - sourceKind: CaptureSourceKind.manual, - createdAt: DateTime(2026, 2, 12, 14, 30), - updatedAt: DateTime(2026, 2, 12, 14, 30), - ), - ], + static LocalDataState empty() { + return const LocalDataState( + people: [], + moments: [], + ideas: [], + reminders: [], + tasks: [], ); } + + static LocalDataState seed() => empty(); } diff --git a/lib/core/config/app_config.dart b/lib/core/config/app_config.dart index f81223d..54f9194 100644 --- a/lib/core/config/app_config.dart +++ b/lib/core/config/app_config.dart @@ -33,7 +33,7 @@ class AppConfig { /// Enables periodic startup/resume sync triggers while authenticated. static bool get enableBackgroundSync => - const bool.fromEnvironment('ENABLE_BACKGROUND_SYNC', defaultValue: true); + const bool.fromEnvironment('ENABLE_BACKGROUND_SYNC', defaultValue: false); /// Enables local notifications runtime for reminder delivery. static bool get enableLocalNotifications => const bool.fromEnvironment( diff --git a/lib/features/ai_digest/application/anonymized_llm_context_builder.dart b/lib/features/ai_digest/application/anonymized_llm_context_builder.dart index c9eb8b1..2b2b4c9 100644 --- a/lib/features/ai_digest/application/anonymized_llm_context_builder.dart +++ b/lib/features/ai_digest/application/anonymized_llm_context_builder.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:math' as math; import 'package:relationship_saver/app/state/local_data_state.dart'; +import 'package:relationship_saver/features/ai_digest/domain/ai_digest_models.dart'; import 'package:relationship_saver/features/people/domain/person_models.dart'; class AnonymizedLlmDigestContext { @@ -22,11 +23,13 @@ class AnonymizedLlmContextBuilder { const AnonymizedLlmContextBuilder({ this.maxPeople = 20, this.maxSignalsPerPerson = 8, + this.maxPriorSuggestionsPerPerson = 6, DateTime Function()? now, }) : _now = now ?? DateTime.now; final int maxPeople; final int maxSignalsPerPerson; + final int maxPriorSuggestionsPerPerson; final DateTime Function() _now; AnonymizedLlmDigestContext build(LocalDataState state) { @@ -66,6 +69,7 @@ class AnonymizedLlmContextBuilder { 'Do not infer or ask for names.', 'Return JSON only.', 'Prefer practical suggestions that can be reviewed before saving.', + 'Do not repeat priorSuggestions for the same personToken.', ], 'limits': { 'maxItems': 10, @@ -184,9 +188,27 @@ class AnonymizedLlmContextBuilder { .map((PersonPreferenceSignal signal) => 'avoid ${signal.label}') .toList(growable: false), ).take(maxSignalsPerPerson).toList(growable: false), + 'priorSuggestions': _priorSuggestions(state, person), }; } + List> _priorSuggestions( + LocalDataState state, + PersonProfile person, + ) { + return state.aiSuggestionDrafts + .where((AiSuggestionDraft draft) => draft.personId == person.id) + .take(maxPriorSuggestionsPerPerson) + .map( + (AiSuggestionDraft draft) => { + 'kind': draft.kind.name, + 'status': draft.status.name, + 'title': _safeCategory(draft.title), + }, + ) + .toList(growable: false); + } + String _relationshipCategory(String value) { final String normalized = value.toLowerCase(); if (_containsAny(normalized, [ diff --git a/lib/features/people/presentation/people_view_detail.dart b/lib/features/people/presentation/people_view_detail.dart index 602b852..07ba287 100644 --- a/lib/features/people/presentation/people_view_detail.dart +++ b/lib/features/people/presentation/people_view_detail.dart @@ -391,11 +391,11 @@ class _PersonDetail extends ConsumerWidget { ] : sharedMessages .take(compact ? 3 : 5) - .map( + .map( (SharedMessageEntry entry) => _PeopleInsightRow( title: entry.sourceDisplayName?.trim().isNotEmpty == true - ? entry.sourceDisplayName!.trim() + ? 'Detected in text: ${entry.sourceDisplayName!.trim()}' : 'Shared from ${entry.sourceApp}', subtitle: entry.messageText.trim().isEmpty ? 'Imported message content is empty' @@ -404,6 +404,23 @@ class _PersonDetail extends ConsumerWidget { '${entry.resolvedAutomatically ? 'AUTO-LINKED' : 'MANUAL-LINK'} • ${_shortDateTimeLabel(entry.sharedAt)}', ), ) + .followedBy([ + if (sharedMessages.length > (compact ? 3 : 5)) + Align( + alignment: Alignment.centerLeft, + child: TextButton.icon( + onPressed: () => _showSharedMessageHistory( + context, + person: person, + sharedMessages: sharedMessages, + ), + icon: const Icon(Icons.list_alt_rounded), + label: Text( + 'View all ${sharedMessages.length} shared texts', + ), + ), + ), + ]) .toList(growable: false), ), ], @@ -411,6 +428,70 @@ class _PersonDetail extends ConsumerWidget { } } +Future _showSharedMessageHistory( + BuildContext context, { + required PersonProfile person, + required List sharedMessages, +}) { + return showModalBottomSheet( + context: context, + useSafeArea: true, + isScrollControlled: true, + showDragHandle: true, + builder: (BuildContext context) { + return DraggableScrollableSheet( + expand: false, + initialChildSize: 0.82, + minChildSize: 0.45, + maxChildSize: 0.95, + builder: (BuildContext context, ScrollController scrollController) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Shared texts', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 4), + Text( + '${person.name} • ${sharedMessages.length} item${sharedMessages.length == 1 ? '' : 's'}', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: AppTheme.textSecondary, + ), + ), + const SizedBox(height: 12), + Expanded( + child: ListView.separated( + controller: scrollController, + itemCount: sharedMessages.length, + separatorBuilder: (_, _) => const SizedBox(height: 10), + itemBuilder: (BuildContext context, int index) { + final SharedMessageEntry entry = sharedMessages[index]; + return _PeopleInsightRow( + title: + entry.sourceDisplayName?.trim().isNotEmpty == true + ? 'Detected in text: ${entry.sourceDisplayName!.trim()}' + : 'Shared from ${entry.sourceApp}', + subtitle: entry.messageText.trim().isEmpty + ? entry.url ?? 'Imported message content is empty' + : entry.messageText.trim(), + meta: + '${entry.sourceApp.toUpperCase()} • ${entry.resolvedAutomatically ? 'AUTO-LINKED' : 'MANUAL-LINK'} • ${_shortDateTimeLabel(entry.sharedAt)}', + ); + }, + ), + ), + ], + ), + ); + }, + ); + }, + ); +} + Future _showPeoplePreferenceEvidence( BuildContext context, { required PersonPreferenceSignal signal, diff --git a/lib/features/settings/presentation/settings_view.dart b/lib/features/settings/presentation/settings_view.dart index 8e55c63..b770292 100644 --- a/lib/features/settings/presentation/settings_view.dart +++ b/lib/features/settings/presentation/settings_view.dart @@ -10,6 +10,7 @@ import 'package:relationship_saver/features/ai_digest/presentation/ai_digest_rev import 'package:relationship_saver/features/auth/session_controller.dart'; import 'package:relationship_saver/features/local/local_models.dart'; import 'package:relationship_saver/features/reminders/scheduling/reminder_scheduler_provider.dart'; +import 'package:relationship_saver/features/share_intake/application/share_intake_debug_log.dart'; import 'package:relationship_saver/features/share_intake/share_capture_flow.dart'; import 'package:relationship_saver/features/share_intake/share_capture_models.dart'; import 'package:relationship_saver/features/share_intake/share_inbox_view.dart'; @@ -210,6 +211,11 @@ class SettingsView extends ConsumerWidget { icon: const Icon(Icons.inbox_rounded), label: const Text('Open Share Inbox'), ), + OutlinedButton.icon( + onPressed: () => _showShareDiagnostics(context), + icon: const Icon(Icons.bug_report_rounded), + label: const Text('Share Diagnostics'), + ), ], ), ], @@ -411,6 +417,26 @@ class SettingsView extends ConsumerWidget { ), ); } + + Future _showShareDiagnostics(BuildContext context) async { + final ShareIntakeDebugLog debugLog = const ShareIntakeDebugLog(); + final List events = await debugLog.read(); + if (!context.mounted) { + return; + } + await showDialog( + context: context, + builder: (BuildContext context) => _ShareDiagnosticsDialog( + events: events, + onClear: () async { + await debugLog.clear(); + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + ), + ); + } } class _SettingRow extends StatelessWidget { @@ -462,6 +488,57 @@ class _SettingRow extends StatelessWidget { } } +class _ShareDiagnosticsDialog extends StatelessWidget { + const _ShareDiagnosticsDialog({required this.events, required this.onClear}); + + final List events; + final Future Function() onClear; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Share Diagnostics'), + content: SizedBox( + width: 520, + child: events.isEmpty + ? const Text( + 'No Flutter share events recorded yet. If you tried sharing, the native share extension likely did not open the app or the plugin did not receive the URL callback.', + ) + : SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: events + .map( + (ShareIntakeDebugEvent event) => Padding( + padding: const EdgeInsets.only(bottom: 10), + child: SelectableText( + '${_debugTimeLabel(event.at)} ${event.stage}\n${event.message}', + ), + ), + ) + .toList(growable: false), + ), + ), + ), + actions: [ + TextButton( + onPressed: events.isEmpty ? null : onClear, + child: const Text('Clear'), + ), + FilledButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Close'), + ), + ], + ); + } +} + +String _debugTimeLabel(DateTime value) { + return '${value.hour.toString().padLeft(2, '0')}:${value.minute.toString().padLeft(2, '0')}:${value.second.toString().padLeft(2, '0')}'; +} + class _LlmConfigDraft { const _LlmConfigDraft({ required this.provider, diff --git a/lib/features/share_intake/application/share_capture_flow.dart b/lib/features/share_intake/application/share_capture_flow.dart index 130006d..b8ac798 100644 --- a/lib/features/share_intake/application/share_capture_flow.dart +++ b/lib/features/share_intake/application/share_capture_flow.dart @@ -39,12 +39,7 @@ Future startShareCaptureReview( required WidgetRef ref, required SharedPayload payload, }) { - return showShareCaptureReviewSheet( - context, - ref: ref, - payload: payload, - initialPersonId: ref.read(selectedPersonIdProvider), - ); + return showShareCaptureReviewSheet(context, ref: ref, payload: payload); } void openShareCaptureDestination( @@ -73,10 +68,10 @@ void openShareCaptureDestination( String shareCaptureResultMessage(SharedMessageIngestResult result) { if (result.isQueuedForResolution) { - return 'Saved shared capture to inbox.'; + return 'Saved shared text to Capture inbox.'; } if (result.createdProfile) { - return 'Saved shared capture and created ${result.profileName}.'; + return 'Saved shared text and created ${result.profileName}.'; } - return 'Saved shared capture to ${result.profileName}.'; + return 'Saved shared text to ${result.profileName}.'; } diff --git a/lib/features/share_intake/application/share_intake_debug_log.dart b/lib/features/share_intake/application/share_intake_debug_log.dart new file mode 100644 index 0000000..a7e5368 --- /dev/null +++ b/lib/features/share_intake/application/share_intake_debug_log.dart @@ -0,0 +1,79 @@ +import 'dart:convert'; + +import 'package:shared_preferences/shared_preferences.dart'; + +const String _shareIntakeDebugLogKey = 'share_intake_debug_log_v1'; +const int _maxShareIntakeDebugEvents = 40; + +class ShareIntakeDebugEvent { + const ShareIntakeDebugEvent({ + required this.at, + required this.stage, + required this.message, + }); + + factory ShareIntakeDebugEvent.fromJson(Map json) { + return ShareIntakeDebugEvent( + at: DateTime.parse( + json['at'] as String? ?? DateTime.now().toUtc().toIso8601String(), + ).toLocal(), + stage: json['stage'] as String? ?? 'unknown', + message: json['message'] as String? ?? '', + ); + } + + final DateTime at; + final String stage; + final String message; + + Map toJson() { + return { + 'at': at.toUtc().toIso8601String(), + 'stage': stage, + 'message': message, + }; + } +} + +class ShareIntakeDebugLog { + const ShareIntakeDebugLog(); + + Future> read() async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + final String? raw = prefs.getString(_shareIntakeDebugLogKey); + if (raw == null || raw.isEmpty) { + return const []; + } + try { + final List items = jsonDecode(raw) as List; + return items + .whereType>() + .map(ShareIntakeDebugEvent.fromJson) + .toList(growable: false); + } on FormatException { + return const []; + } + } + + Future record(String stage, String message) async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + final List current = await read(); + final List next = [ + ShareIntakeDebugEvent(at: DateTime.now(), stage: stage, message: message), + ...current, + ].take(_maxShareIntakeDebugEvents).toList(growable: false); + await prefs.setString( + _shareIntakeDebugLogKey, + jsonEncode( + next + .map((ShareIntakeDebugEvent event) => event.toJson()) + .toList(growable: false), + ), + ); + } + + Future clear() async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.remove(_shareIntakeDebugLogKey); + } +} diff --git a/lib/features/share_intake/presentation/incoming_share_listener.dart b/lib/features/share_intake/presentation/incoming_share_listener.dart index 8386234..8489d6e 100644 --- a/lib/features/share_intake/presentation/incoming_share_listener.dart +++ b/lib/features/share_intake/presentation/incoming_share_listener.dart @@ -6,6 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:relationship_saver/core/config/app_config.dart'; import 'package:relationship_saver/features/local/local_models.dart'; +import 'package:relationship_saver/features/share_intake/application/share_intake_debug_log.dart'; import 'package:relationship_saver/features/share_intake/share_capture_flow.dart'; import 'package:relationship_saver/features/share_intake/share_capture_models.dart'; @@ -23,6 +24,7 @@ class IncomingShareListener extends ConsumerStatefulWidget { class _IncomingShareListenerState extends ConsumerState { StreamSubscription>? _mediaSubscription; String? _lastToken; + final ShareIntakeDebugLog _debugLog = const ShareIntakeDebugLog(); @override void initState() { @@ -41,25 +43,44 @@ class _IncomingShareListenerState extends ConsumerState { Future _start() async { if (!AppConfig.enableWhatsAppShareIntake || kIsWeb) { + await _debugLog.record( + 'listener_skipped', + 'enabled=${AppConfig.enableWhatsAppShareIntake}, kIsWeb=$kIsWeb', + ); return; } try { + await _debugLog.record('listener_start', 'Subscribing to share intents.'); _mediaSubscription = ReceiveSharingIntent.instance .getMediaStream() .listen( - (List files) => - _handleMediaList(files, autoOpenDestination: false), - onError: (_) {}, + (List files) { + unawaited( + _debugLog.record( + 'media_stream', + 'Received ${files.length} shared item(s).', + ), + ); + unawaited(_handleMediaList(files, autoOpenDestination: false)); + }, + onError: (Object error) { + unawaited(_debugLog.record('media_stream_error', '$error')); + }, ); final List initialMedia = await ReceiveSharingIntent .instance .getInitialMedia(); + await _debugLog.record( + 'initial_media', + 'Received ${initialMedia.length} initial shared item(s).', + ); if (initialMedia.isNotEmpty) { await _handleMediaList(initialMedia, autoOpenDestination: true); } - } catch (_) { + } catch (error) { + await _debugLog.record('listener_error', '$error'); // Unsupported platform/plugin state should not break app usage. } } @@ -69,8 +90,16 @@ class _IncomingShareListenerState extends ConsumerState { required bool autoOpenDestination, }) async { for (final SharedMediaFile file in files) { + await _debugLog.record( + 'media_item', + 'type=${file.type.name}, mime=${file.mimeType ?? 'none'}, pathLength=${file.path.length}, messageLength=${file.message?.length ?? 0}', + ); final String? rawText = _extractText(file); if (rawText == null) { + await _debugLog.record( + 'extract_skip', + 'No usable text for type=${file.type.name}.', + ); continue; } await _handleRawText( @@ -129,12 +158,21 @@ class _IncomingShareListenerState extends ConsumerState { sourceAppHint: sourceAppHint, ); if (payload == null) { + await _debugLog.record( + 'payload_skip', + 'Parser returned null for rawLength=${trimmed.length}.', + ); return; } + await _debugLog.record( + 'payload_parsed', + 'source=${payload.sourceApp}, type=${payload.payloadType.name}, rawLength=${payload.rawText.length}, hasUrl=${payload.url != null}, detectedSender=${payload.sourceDisplayName != null}', + ); final String token = '${payload.sourceApp}:${payload.sourceUserId ?? payload.sourceDisplayName ?? ''}:${payload.rawText}:${payload.url ?? ''}'; if (_lastToken == token) { + await _debugLog.record('payload_duplicate', 'Ignored duplicate share.'); return; } _lastToken = token; @@ -149,8 +187,17 @@ class _IncomingShareListenerState extends ConsumerState { ); if (!mounted || result == null) { + await _debugLog.record('review_cancelled', 'Share review closed.'); return; } + unawaited( + _debugLog.record( + 'review_result', + result.isQueuedForResolution + ? 'Queued for inbox.' + : 'Saved to profile; createdProfile=${result.createdProfile}.', + ), + ); if (autoOpenDestination) { openShareCaptureDestination(context, ref: ref, result: result); diff --git a/lib/features/share_intake/presentation/share_capture_review_sheet.dart b/lib/features/share_intake/presentation/share_capture_review_sheet.dart index 2bf5b02..a25a164 100644 --- a/lib/features/share_intake/presentation/share_capture_review_sheet.dart +++ b/lib/features/share_intake/presentation/share_capture_review_sheet.dart @@ -88,12 +88,12 @@ class _ShareCaptureReviewSheetState crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Save Shared Capture', + 'Save Shared Text', style: Theme.of(context).textTheme.headlineSmall, ), const SizedBox(height: 6), Text( - 'Review the incoming text or link, then assign it safely.', + 'Choose who this belongs to, or keep it in the inbox for later review.', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: AppTheme.textSecondary, ), @@ -101,10 +101,7 @@ class _ShareCaptureReviewSheetState const SizedBox(height: 14), _PayloadPreview(payload: widget.payload), const SizedBox(height: 14), - Text( - 'What kind of info is this?', - style: Theme.of(context).textTheme.titleMedium, - ), + Text('Type', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 8), Wrap( spacing: 8, @@ -344,9 +341,7 @@ class _ShareCaptureReviewSheetState await showDialog<_CreatePersonFromShareDraft>( context: context, builder: (BuildContext context) => _CreatePersonFromShareDialog( - initialName: - widget.payload.sourceDisplayName ?? - widget.payload.rawText.split(RegExp(r'\s+')).take(2).join(' '), + detectedName: widget.payload.sourceDisplayName, ), ); if (draft == null) { @@ -445,7 +440,7 @@ class _PayloadPreview extends StatelessWidget { if (payload.sourceDisplayName != null) ...[ const SizedBox(height: 4), Text( - 'Sender: ${payload.sourceDisplayName}', + 'Detected in text: ${payload.sourceDisplayName}', style: Theme.of( context, ).textTheme.bodySmall?.copyWith(color: AppTheme.textSecondary), @@ -484,9 +479,9 @@ class _CreatePersonFromShareDraft { } class _CreatePersonFromShareDialog extends StatefulWidget { - const _CreatePersonFromShareDialog({required this.initialName}); + const _CreatePersonFromShareDialog({this.detectedName}); - final String initialName; + final String? detectedName; @override State<_CreatePersonFromShareDialog> createState() => @@ -504,7 +499,7 @@ class _CreatePersonFromShareDialogState @override void initState() { super.initState(); - _nameController = TextEditingController(text: widget.initialName); + _nameController = TextEditingController(); } @override @@ -523,9 +518,21 @@ class _CreatePersonFromShareDialogState child: Column( mainAxisSize: MainAxisSize.min, children: [ + if (widget.detectedName?.trim().isNotEmpty == true) ...[ + Text( + 'Detected message author: ${widget.detectedName!.trim()}', + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(color: AppTheme.textSecondary), + ), + const SizedBox(height: 10), + ], TextField( controller: _nameController, - decoration: const InputDecoration(labelText: 'Name'), + decoration: const InputDecoration( + labelText: 'Person name', + hintText: 'Enter the real contact', + ), ), const SizedBox(height: 10), TextField( diff --git a/lib/features/share_intake/presentation/share_inbox_view.dart b/lib/features/share_intake/presentation/share_inbox_view.dart index af1d85b..6eb3ee1 100644 --- a/lib/features/share_intake/presentation/share_inbox_view.dart +++ b/lib/features/share_intake/presentation/share_inbox_view.dart @@ -4,6 +4,8 @@ import 'package:relationship_saver/core/config/app_theme.dart'; import 'package:relationship_saver/features/local/local_models.dart'; import 'package:relationship_saver/features/local/local_repository.dart'; import 'package:relationship_saver/features/share_intake/share_capture_models.dart'; +import 'package:relationship_saver/features/share_intake/share_capture_review_sheet.dart'; +import 'package:relationship_saver/features/share_intake/share_payload_parser.dart'; import 'package:relationship_saver/features/shared/frosted_card.dart'; /// Review unresolved shared messages and map them to a person profile. @@ -45,13 +47,25 @@ class _ShareInboxViewState extends ConsumerState { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'Share Inbox', - style: Theme.of(context).textTheme.headlineMedium, + Wrap( + spacing: 12, + runSpacing: 10, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Text( + 'Capture', + style: Theme.of(context).textTheme.headlineMedium, + ), + FilledButton.icon( + onPressed: _submitting ? null : _addTextCapture, + icon: const Icon(Icons.edit_note_rounded), + label: const Text('Add Text'), + ), + ], ), const SizedBox(height: 6), Text( - 'Resolve shared messages that could not be matched confidently.', + 'Share messages here, paste copied text, then choose or create the right person.', style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: AppTheme.textSecondary, ), @@ -59,7 +73,9 @@ class _ShareInboxViewState extends ConsumerState { const SizedBox(height: 14), Expanded( child: localState.sharedInbox.isEmpty - ? _EmptyInbox(compact: compact) + ? SingleChildScrollView( + child: _EmptyInbox(compact: compact), + ) : ListView.separated( itemCount: localState.sharedInbox.length, separatorBuilder: (_, _) => @@ -178,13 +194,8 @@ class _ShareInboxViewState extends ConsumerState { } final _CreateProfileDraft? draft = await showDialog<_CreateProfileDraft>( context: context, - builder: (BuildContext context) => _CreateProfileDialog( - initialName: - entry.sourceDisplayName ?? - entry.sourceUserId ?? - entry.sourceThreadId ?? - '', - ), + builder: (BuildContext context) => + _CreateProfileDialog(detectedName: entry.sourceDisplayName), ); if (draft == null || !mounted) { return; @@ -232,6 +243,38 @@ class _ShareInboxViewState extends ConsumerState { }); } + Future _addTextCapture() async { + if (_submitting) { + return; + } + final String? rawText = await showDialog( + context: context, + builder: (BuildContext context) => const _AddTextDialog(), + ); + if (rawText == null || rawText.trim().isEmpty || !mounted) { + return; + } + + final SharedPayload payload = SharePayloadParser.parse( + rawText: rawText, + sourceApp: 'manual', + platform: 'manual', + createdAt: DateTime.now(), + receivedAt: DateTime.now(), + ); + final SharedMessageIngestResult? result = await showShareCaptureReviewSheet( + context, + ref: ref, + payload: payload, + ); + if (result == null || !mounted) { + return; + } + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(_captureResultMessage(result)))); + } + Future _runRepositoryAction(Future Function() action) async { setState(() { _submitting = true; @@ -453,6 +496,60 @@ class _ShareInboxViewState extends ConsumerState { } } +class _AddTextDialog extends StatefulWidget { + const _AddTextDialog(); + + @override + State<_AddTextDialog> createState() => _AddTextDialogState(); +} + +class _AddTextDialogState extends State<_AddTextDialog> { + late final TextEditingController _controller; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Add Text'), + content: SizedBox( + width: 520, + child: TextField( + controller: _controller, + autofocus: true, + minLines: 5, + maxLines: 10, + textInputAction: TextInputAction.newline, + decoration: const InputDecoration( + labelText: 'Shared or copied text', + hintText: 'Paste a message, note, link, preference, or date...', + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + FilledButton( + onPressed: () => Navigator.of(context).pop(_controller.text), + child: const Text('Review'), + ), + ], + ); + } +} + class _CandidateReview { const _CandidateReview({ required this.person, @@ -1073,6 +1170,16 @@ String _factTypeLabel(CapturedFactType type) { }; } +String _captureResultMessage(SharedMessageIngestResult result) { + if (result.isQueuedForResolution) { + return 'Saved to Capture inbox.'; + } + if (result.createdProfile) { + return 'Saved and created ${result.profileName}.'; + } + return 'Saved to ${result.profileName}.'; +} + class _EmptyInbox extends StatelessWidget { const _EmptyInbox({required this.compact}); @@ -1103,7 +1210,7 @@ class _EmptyInbox extends StatelessWidget { ), const SizedBox(height: 6), Text( - 'Incoming shared captures that need manual person mapping or fact triage will appear here.', + 'Share a message into the app or use Add Text to start building your private relationship notes.', style: Theme.of( context, ).textTheme.bodyMedium?.copyWith(color: AppTheme.textSecondary), @@ -1132,9 +1239,9 @@ class _CreateProfileDraft { } class _CreateProfileDialog extends StatefulWidget { - const _CreateProfileDialog({required this.initialName}); + const _CreateProfileDialog({this.detectedName}); - final String initialName; + final String? detectedName; @override State<_CreateProfileDialog> createState() => _CreateProfileDialogState(); @@ -1149,7 +1256,7 @@ class _CreateProfileDialogState extends State<_CreateProfileDialog> { @override void initState() { super.initState(); - _nameController = TextEditingController(text: widget.initialName); + _nameController = TextEditingController(); _relationshipController = TextEditingController(text: 'Contact'); _locationController = TextEditingController(); _aliasesController = TextEditingController(); @@ -1172,9 +1279,21 @@ class _CreateProfileDialogState extends State<_CreateProfileDialog> { child: Column( mainAxisSize: MainAxisSize.min, children: [ + if (widget.detectedName?.trim().isNotEmpty == true) ...[ + Text( + 'Detected message author: ${widget.detectedName!.trim()}', + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(color: AppTheme.textSecondary), + ), + const SizedBox(height: 8), + ], TextField( controller: _nameController, - decoration: const InputDecoration(labelText: 'Name'), + decoration: const InputDecoration( + labelText: 'Person name', + hintText: 'Enter the real contact', + ), ), const SizedBox(height: 8), TextField( diff --git a/lib/integrations/backend/backend_gateway_fake.dart b/lib/integrations/backend/backend_gateway_fake.dart index aca0825..e4b52aa 100644 --- a/lib/integrations/backend/backend_gateway_fake.dart +++ b/lib/integrations/backend/backend_gateway_fake.dart @@ -97,21 +97,9 @@ class BackendGatewayFake implements BackendGateway { @override Future pullChanges({String? cursor, int limit = 200}) async { - final List changes = [ - ChangeEnvelope( - schemaVersion: 1, - entityType: 'person', - entityId: 'person-1', - op: ChangeOperation.upsert, - modifiedAt: _seedTime, - clientMutationId: 'cm-1', - payload: const {'name': 'Alex', 'importance': 5}, - ), - ]; - return SyncPullResult( - cursor: 'fake-pull-cursor-1', - changes: changes.take(limit).toList(), + cursor: cursor ?? 'fake-pull-cursor-empty', + changes: const [], ); } diff --git a/test/features/ai_digest/ai_digest_repository_test.dart b/test/features/ai_digest/ai_digest_repository_test.dart index ca1c252..e444d35 100644 --- a/test/features/ai_digest/ai_digest_repository_test.dart +++ b/test/features/ai_digest/ai_digest_repository_test.dart @@ -21,10 +21,7 @@ void main() { final ProviderContainer container = _createContainer(); addTearDown(container.dispose); - final LocalDataState initial = await container.read( - localRepositoryProvider.future, - ); - final String personId = initial.people.first.id; + final String personId = await _addTestPerson(container); await container .read(localRepositoryProvider.notifier) @@ -65,10 +62,10 @@ void main() { final ProviderContainer container = _createContainer(); addTearDown(container.dispose); + final String personId = await _addTestPerson(container); final LocalDataState initial = await container.read( localRepositoryProvider.future, ); - final String personId = initial.people.first.id; final int ideaCount = initial.ideas.length; await container @@ -112,3 +109,19 @@ ProviderContainer _createContainer() { ], ); } + +Future _addTestPerson(ProviderContainer container) async { + await container.read(localRepositoryProvider.future); + await container + .read(localRepositoryProvider.notifier) + .addPerson( + name: 'Taylor Quinn', + relationship: 'Friend', + notes: '', + tags: const [], + ); + final LocalDataState state = await container.read( + localRepositoryProvider.future, + ); + return state.people.single.id; +} diff --git a/test/features/ai_digest/anonymized_llm_context_builder_test.dart b/test/features/ai_digest/anonymized_llm_context_builder_test.dart index b68e91a..d8827d0 100644 --- a/test/features/ai_digest/anonymized_llm_context_builder_test.dart +++ b/test/features/ai_digest/anonymized_llm_context_builder_test.dart @@ -1,6 +1,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:relationship_saver/app/state/local_data_state.dart'; import 'package:relationship_saver/features/ai_digest/application/anonymized_llm_context_builder.dart'; +import 'package:relationship_saver/features/ai_digest/domain/ai_digest_models.dart'; import 'package:relationship_saver/features/dashboard/domain/dashboard_models.dart'; import 'package:relationship_saver/features/ideas/domain/idea_models.dart'; import 'package:relationship_saver/features/moments/domain/moment_models.dart'; @@ -73,6 +74,19 @@ void main() { isSensitive: true, ), ], + aiSuggestionDrafts: [ + AiSuggestionDraft( + id: 'ai-1', + personId: 'p-1', + kind: AiSuggestionKind.eventIdea, + title: 'Jazz concert downtown', + details: 'Should not need to repeat the details.', + suggestedAt: DateTime(2026, 5, 10), + confidence: 0.8, + status: AiSuggestionStatus.dismissed, + sourceRunId: 'digest-old', + ), + ], ); final AnonymizedLlmDigestContext context = AnonymizedLlmContextBuilder( @@ -90,5 +104,9 @@ void main() { expect(prompt, isNot(contains('thread-secret'))); expect(prompt, isNot(contains('Raw private chat text'))); expect(prompt, isNot(contains('Secret medical detail'))); + expect(prompt, contains('priorSuggestions')); + expect(prompt, contains('jazz concert downtown')); + expect(prompt, contains('dismissed')); + expect(prompt, isNot(contains('Should not need to repeat the details.'))); }); } diff --git a/test/features/ai_digest/llm_digest_orchestrator_test.dart b/test/features/ai_digest/llm_digest_orchestrator_test.dart index 75f88e9..ebf5920 100644 --- a/test/features/ai_digest/llm_digest_orchestrator_test.dart +++ b/test/features/ai_digest/llm_digest_orchestrator_test.dart @@ -48,6 +48,15 @@ void main() { ); addTearDown(container.dispose); + await container.read(localRepositoryProvider.future); + await container + .read(localRepositoryProvider.notifier) + .addPerson( + name: 'Taylor Quinn', + relationship: 'Friend', + notes: '', + tags: const ['tea'], + ); await container.read(llmDigestConfigProvider.notifier).setEnabled(true); final LlmDigestRunResult result = await container diff --git a/test/features/app/app_auth_flow_test.dart b/test/features/app/app_auth_flow_test.dart index 7e6d777..639cc77 100644 --- a/test/features/app/app_auth_flow_test.dart +++ b/test/features/app/app_auth_flow_test.dart @@ -40,7 +40,7 @@ void main() { await tester.tap(find.widgetWithText(FilledButton, 'Sign In')); await tester.pumpAndSettle(const Duration(milliseconds: 450)); - expect(find.text('Dashboard'), findsWidgets); + expect(find.text('Capture'), findsWidgets); await tester.tap(find.text('Settings').first); await tester.pumpAndSettle(const Duration(milliseconds: 450)); diff --git a/test/features/app/app_mobile_crud_sync_flow_test.dart b/test/features/app/app_mobile_crud_sync_flow_test.dart index 3a3b548..930c5ab 100644 --- a/test/features/app/app_mobile_crud_sync_flow_test.dart +++ b/test/features/app/app_mobile_crud_sync_flow_test.dart @@ -47,7 +47,7 @@ void main() { await tester.tap(find.widgetWithText(FilledButton, 'Sign In')); await tester.pumpAndSettle(const Duration(milliseconds: 500)); - expect(find.text('Dashboard'), findsWidgets); + expect(find.text('Capture'), findsWidgets); await tester.tap(find.text('People').last); await tester.pumpAndSettle(const Duration(milliseconds: 500)); @@ -91,13 +91,5 @@ void main() { }), isTrue, ); - - await tester.tap(find.byTooltip('More')); - await tester.pumpAndSettle(const Duration(milliseconds: 250)); - await tester.tap(find.text('Sync').last); - await tester.pumpAndSettle(const Duration(milliseconds: 500)); - - expect(find.text('Queue Status'), findsOneWidget); - expect(find.text('Pending changes'), findsOneWidget); }); } diff --git a/test/features/dashboard/dashboard_graph_interactions_test.dart b/test/features/dashboard/dashboard_graph_interactions_test.dart index 25e97be..0f8b407 100644 --- a/test/features/dashboard/dashboard_graph_interactions_test.dart +++ b/test/features/dashboard/dashboard_graph_interactions_test.dart @@ -25,13 +25,23 @@ void main() { ); addTearDown(container.dispose); + await container.read(localRepositoryProvider.future); + await container + .read(localRepositoryProvider.notifier) + .addPerson( + name: 'Ava Hart', + relationship: 'Friend', + notes: '', + tags: const [], + ); final LocalDataState seeded = await container.read( localRepositoryProvider.future, ); + final String personId = seeded.people.first.id; await container .read(localRepositoryProvider.notifier) .upsertInferredPreferenceSignalObservation( - personId: seeded.people.first.id, + personId: personId, key: 'drink:tea', label: 'Tea', category: 'drink', @@ -58,7 +68,7 @@ void main() { expect(find.text('Explore'), findsOneWidget); final Finder avaNode = find.byKey( - const ValueKey('graph-node-p-ava'), + ValueKey('graph-node-$personId'), ); expect(avaNode, findsOneWidget); diff --git a/test/features/local/local_repository_test.dart b/test/features/local/local_repository_test.dart index b0b7c1a..9e41364 100644 --- a/test/features/local/local_repository_test.dart +++ b/test/features/local/local_repository_test.dart @@ -17,15 +17,17 @@ void main() { SharedPreferences.setMockInitialValues({}); }); - test('seeds local data on first load', () async { + test('starts with empty local data on first load', () async { final ProviderContainer container = _createContainer(); addTearDown(container.dispose); final state = await container.read(localRepositoryProvider.future); - expect(state.people, isNotEmpty); - expect(state.moments, isNotEmpty); - expect(state.tasks, isNotEmpty); + expect(state.people, isEmpty); + expect(state.moments, isEmpty); + expect(state.ideas, isEmpty); + expect(state.reminders, isEmpty); + expect(state.tasks, isEmpty); }); test('adds and removes person with persistence state update', () async { @@ -62,8 +64,8 @@ void main() { final ProviderContainer container = _createContainer(); addTearDown(container.dispose); + final String personId = await _addTestPerson(container); final state = await container.read(localRepositoryProvider.future); - final String personId = state.people.first.id; final int beforeCount = state.moments.length; await container @@ -86,10 +88,7 @@ void main() { final ProviderContainer container = _createContainer(); addTearDown(container.dispose); - final LocalDataState state = await container.read( - localRepositoryProvider.future, - ); - final String personId = state.people.first.id; + final String personId = await _addTestPerson(container); final PersonPreferenceSignal first = await container .read(localRepositoryProvider.notifier) @@ -161,7 +160,7 @@ void main() { }); test( - 'ingestSharedMessage auto-creates profile and reuses source link for follow-up messages', + 'ingestSharedMessage queues unknown identity, then reuses source link after manual creation', () async { final ProviderContainer container = _createContainer(); addTearDown(container.dispose); @@ -184,22 +183,35 @@ void main() { final LocalDataState afterFirst = await container.read( localRepositoryProvider.future, ); - expect(first.createdProfile, isTrue); - expect(afterFirst.people.length, before.people.length + 1); - final SourceProfileLink createdLink = afterFirst.sourceLinks.firstWhere( - (SourceProfileLink link) => link.profileId == first.profileId, + expect(first.isQueuedForResolution, isTrue); + expect(first.createdProfile, isFalse); + expect(afterFirst.people.length, before.people.length); + expect( + afterFirst.sharedInbox.single.reason, + SharedInboxReason.missingIdentity, + ); + + final SharedMessageIngestResult created = await container + .read(localRepositoryProvider.notifier) + .resolveSharedInboxByCreatingProfile( + inboxEntryId: first.inboxEntryId!, + name: 'Taylor Quinn', + relationship: 'Friend', + ); + + final LocalDataState afterCreate = await container.read( + localRepositoryProvider.future, + ); + expect(created.createdProfile, isTrue); + expect(afterCreate.people.length, before.people.length + 1); + final SourceProfileLink createdLink = afterCreate.sourceLinks.firstWhere( + (SourceProfileLink link) => link.profileId == created.profileId, ); expect(createdLink.sourceFingerprint, 'whatsapp|u:wa:taylor quinn'); expect( - afterFirst.sourceLinks.any( - (SourceProfileLink link) => link.profileId == first.profileId, - ), - isTrue, - ); - expect( - afterFirst.moments.any( + afterCreate.moments.any( (RelationshipMoment moment) => - moment.personId == first.profileId && moment.type == 'whatsapp', + moment.personId == created.profileId && moment.type == 'whatsapp', ), isTrue, ); @@ -219,11 +231,11 @@ void main() { localRepositoryProvider.future, ); expect(second.createdProfile, isFalse); - expect(second.profileId, first.profileId); - expect(afterSecond.people.length, afterFirst.people.length); + expect(second.profileId, created.profileId); + expect(afterSecond.people.length, afterCreate.people.length); expect( afterSecond.sharedMessages.length, - afterFirst.sharedMessages.length + 1, + afterCreate.sharedMessages.length + 1, ); }, ); @@ -234,7 +246,7 @@ void main() { final ProviderContainer container = _createContainer(); addTearDown(container.dispose); - await container.read(localRepositoryProvider.future); + await _addTestPerson(container, name: 'Nora Diaz'); final SharedMessageIngestResult result = await container .read(localRepositoryProvider.notifier) @@ -285,10 +297,7 @@ void main() { final ProviderContainer container = _createContainer(); addTearDown(container.dispose); - final LocalDataState state = await container.read( - localRepositoryProvider.future, - ); - final String personId = state.people.first.id; + final String personId = await _addTestPerson(container); final SharedPayload payload = SharePayloadParser.parse( rawText: 'She loves vegan sushi and rooftop dinners.', sourceApp: 'share_sheet', @@ -378,6 +387,35 @@ void main() { }, ); + test( + 'captureSharedPayloadByCreatingProfile requires an explicit person name', + () async { + final ProviderContainer container = _createContainer(); + addTearDown(container.dispose); + + await container.read(localRepositoryProvider.future); + final SharedPayload payload = SharePayloadParser.parse( + rawText: 'Alex: Kaffee klingt gut', + sourceApp: 'whatsapp', + platform: 'ios', + ); + + expect(payload.sourceDisplayName, 'Alex'); + expect( + () => container + .read(localRepositoryProvider.notifier) + .captureSharedPayloadByCreatingProfile(payload: payload), + throwsArgumentError, + ); + + final LocalDataState after = await container.read( + localRepositoryProvider.future, + ); + expect(after.people, isEmpty); + expect(after.sharedMessages, isEmpty); + }, + ); + test( 'queues near-match conflict and then uses fingerprint mapping for follow-up shares', () async { @@ -689,10 +727,10 @@ void main() { final ProviderContainer container = _createContainer(); addTearDown(container.dispose); + final String personId = await _addTestPerson(container); final LocalDataState state = await container.read( localRepositoryProvider.future, ); - final String personId = state.people.first.id; final int beforeCount = state.ideas.length; await container @@ -740,10 +778,10 @@ void main() { final ProviderContainer container = _createContainer(); addTearDown(container.dispose); + final String personId = await _addTestPerson(container); final LocalDataState state = await container.read( localRepositoryProvider.future, ); - final String personId = state.people.first.id; final int beforeCount = state.reminders.length; await container @@ -852,6 +890,28 @@ ProviderContainer _createContainer({ReminderScheduler? scheduler}) { ); } +Future _addTestPerson( + ProviderContainer container, { + String name = 'Test Person', + String relationship = 'Friend', +}) async { + await container.read(localRepositoryProvider.future); + await container + .read(localRepositoryProvider.notifier) + .addPerson( + name: name, + relationship: relationship, + notes: '', + tags: const [], + ); + final LocalDataState state = await container.read( + localRepositoryProvider.future, + ); + return state.people + .firstWhere((PersonProfile person) => person.name == name) + .id; +} + class RecordingReminderScheduler implements ReminderScheduler { final List> snapshots = >[]; diff --git a/test/features/local/storage/local_repository_store_override_test.dart b/test/features/local/storage/local_repository_store_override_test.dart index 699e794..9aeaefd 100644 --- a/test/features/local/storage/local_repository_store_override_test.dart +++ b/test/features/local/storage/local_repository_store_override_test.dart @@ -19,7 +19,7 @@ void main() { addTearDown(container.dispose); final initial = await container.read(localRepositoryProvider.future); - expect(initial.people, isNotEmpty); + expect(initial.people, isEmpty); await container .read(localRepositoryProvider.notifier) diff --git a/test/features/people/people_quick_capture_dialog_test.dart b/test/features/people/people_quick_capture_dialog_test.dart index 31086e6..cf997f4 100644 --- a/test/features/people/people_quick_capture_dialog_test.dart +++ b/test/features/people/people_quick_capture_dialog_test.dart @@ -30,6 +30,15 @@ void main() { ); addTearDown(container.dispose); + await container.read(localRepositoryProvider.future); + await container + .read(localRepositoryProvider.notifier) + .addPerson( + name: 'Taylor Quinn', + relationship: 'Friend', + notes: '', + tags: const [], + ); final LocalDataState initial = await container.read( localRepositoryProvider.future, ); diff --git a/test/features/share_intake/share_inbox_view_test.dart b/test/features/share_intake/share_inbox_view_test.dart index e86371c..ea8e826 100644 --- a/test/features/share_intake/share_inbox_view_test.dart +++ b/test/features/share_intake/share_inbox_view_test.dart @@ -65,7 +65,7 @@ void main() { ); await tester.pumpAndSettle(); - expect(find.text('Share Inbox'), findsOneWidget); + expect(find.text('Capture'), findsOneWidget); expect(find.text('Ambiguous match'), findsOneWidget); expect(find.widgetWithText(FilledButton, 'Review & Save'), findsOneWidget); diff --git a/test/features/sync/sync_background_runner_test.dart b/test/features/sync/sync_background_runner_test.dart index 2f5e11a..3dee781 100644 --- a/test/features/sync/sync_background_runner_test.dart +++ b/test/features/sync/sync_background_runner_test.dart @@ -10,7 +10,7 @@ import 'package:relationship_saver/features/sync/sync_coordinator.dart'; void main() { testWidgets( - 'triggers sync when reachability transitions from offline to online', + 'does not trigger sync when background sync is disabled by default', (WidgetTester tester) async { final _TestReachability reachability = _TestReachability(initial: false); addTearDown(reachability.dispose); @@ -39,56 +39,56 @@ void main() { ); await tester.pumpAndSettle(const Duration(milliseconds: 60)); - // Startup trigger always runs once when background sync is enabled. - expect(syncCalls, 1); + expect(syncCalls, 0); reachability.emit(false); await tester.pumpAndSettle(const Duration(milliseconds: 40)); - expect(syncCalls, 1); + expect(syncCalls, 0); reachability.emit(true); await tester.pumpAndSettle(const Duration(milliseconds: 60)); - expect(syncCalls, 2); + expect(syncCalls, 0); }, ); - testWidgets('does not trigger extra sync while connectivity stays online', ( - WidgetTester tester, - ) async { - final _TestReachability reachability = _TestReachability(initial: true); - addTearDown(reachability.dispose); + testWidgets( + 'does not trigger sync while connectivity stays online by default', + (WidgetTester tester) async { + final _TestReachability reachability = _TestReachability(initial: true); + addTearDown(reachability.dispose); - int syncCalls = 0; + int syncCalls = 0; - await tester.pumpWidget( - ProviderScope( - overrides: [ - networkReachabilityProvider.overrideWithValue(reachability), - syncCoordinatorProvider.overrideWith( - (Ref ref) => _TestSyncCoordinator( - ref, - onSyncNow: () async { - syncCalls += 1; - return _okResult(); - }, + await tester.pumpWidget( + ProviderScope( + overrides: [ + networkReachabilityProvider.overrideWithValue(reachability), + syncCoordinatorProvider.overrideWith( + (Ref ref) => _TestSyncCoordinator( + ref, + onSyncNow: () async { + syncCalls += 1; + return _okResult(); + }, + ), ), + ], + child: const Directionality( + textDirection: TextDirection.ltr, + child: SyncBackgroundRunner(child: SizedBox()), ), - ], - child: const Directionality( - textDirection: TextDirection.ltr, - child: SyncBackgroundRunner(child: SizedBox()), ), - ), - ); - await tester.pumpAndSettle(const Duration(milliseconds: 60)); + ); + await tester.pumpAndSettle(const Duration(milliseconds: 60)); - expect(syncCalls, 1); + expect(syncCalls, 0); - reachability.emit(true); - await tester.pumpAndSettle(const Duration(milliseconds: 60)); + reachability.emit(true); + await tester.pumpAndSettle(const Duration(milliseconds: 60)); - expect(syncCalls, 1); - }); + expect(syncCalls, 0); + }, + ); } class _TestSyncCoordinator extends SyncCoordinator { diff --git a/test/features/sync/sync_coordinator_test.dart b/test/features/sync/sync_coordinator_test.dart index af0b95e..ba1b934 100644 --- a/test/features/sync/sync_coordinator_test.dart +++ b/test/features/sync/sync_coordinator_test.dart @@ -111,6 +111,29 @@ void main() { ); expect(queueState.cursor, 'cursor-remote-1'); }); + + test('default fake pull is inert and does not seed people', () async { + final ProviderContainer container = ProviderContainer( + overrides: [ + backendGatewayProvider.overrideWithValue(BackendGatewayFake()), + localDataStoreProvider.overrideWithValue(InMemoryLocalDataStore()), + syncStateStoreProvider.overrideWithValue(InMemorySyncStateStore()), + ], + ); + addTearDown(container.dispose); + + await container.read(localRepositoryProvider.future); + + final SyncRunResult result = await container + .read(syncCoordinatorProvider) + .pullRemote(); + + expect(result.success, isTrue); + expect(result.pulled, 0); + + final localState = await container.read(localRepositoryProvider.future); + expect(localState.people, isEmpty); + }); } class _TestGateway extends BackendGatewayFake { diff --git a/test/features/sync/sync_rejection_repair_flow_test.dart b/test/features/sync/sync_rejection_repair_flow_test.dart index b226442..397a297 100644 --- a/test/features/sync/sync_rejection_repair_flow_test.dart +++ b/test/features/sync/sync_rejection_repair_flow_test.dart @@ -23,6 +23,15 @@ void main() { ); addTearDown(container.dispose); + await container.read(localRepositoryProvider.future); + await container + .read(localRepositoryProvider.notifier) + .addPerson( + name: 'Repair Person', + relationship: 'Friend', + notes: '', + tags: const [], + ); final localBefore = await container.read(localRepositoryProvider.future); final person = localBefore.people.first;