diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..7a118b4 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source "https://rubygems.org" + +gem "fastlane" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..5da9d11 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,338 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.8) + abbrev (0.1.2) + addressable (2.8.9) + public_suffix (>= 2.0.2, < 8.0) + artifactory (3.0.17) + atomos (0.1.3) + aws-eventstream (1.4.0) + aws-partitions (1.1229.0) + aws-sdk-core (3.244.0) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + base64 + bigdecimal + jmespath (~> 1, >= 1.6.1) + logger + aws-sdk-kms (1.123.0) + aws-sdk-core (~> 3, >= 3.244.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.217.0) + aws-sdk-core (~> 3, >= 3.244.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.12.1) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + base64 (0.2.0) + benchmark (0.5.0) + bigdecimal (4.0.1) + claide (1.1.0) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + csv (3.3.5) + declarative (0.0.20) + digest-crc (0.7.0) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.6.20240107) + dotenv (2.8.1) + emoji_regex (3.2.3) + excon (0.112.0) + faraday (1.10.5) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-cookie_jar (0.0.8) + faraday (>= 0.8.0) + http-cookie (>= 1.0.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.1) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.2.0) + multipart-post (~> 2.0) + faraday-net_http (1.0.2) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.4) + faraday_middleware (1.2.1) + faraday (~> 1.0) + fastimage (2.4.1) + fastlane (2.232.2) + CFPropertyList (>= 2.3, < 4.0.0) + abbrev (~> 0.1.2) + addressable (>= 2.8, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.197) + babosa (>= 1.0.3, < 2.0.0) + base64 (~> 0.2.0) + benchmark (>= 0.1.0) + bundler (>= 1.17.3, < 5.0.0) + colored (~> 1.2) + commander (~> 4.6) + csv (~> 3.3) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, <= 2.1.1) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + http-cookie (~> 1.0.5) + json (< 3.0.0) + jwt (>= 2.1.0, < 3) + logger (>= 1.6, < 2.0) + mini_magick (>= 4.9.4, < 5.0.0) + multipart-post (>= 2.0.0, < 3.0.0) + mutex_m (~> 0.3.0) + naturally (~> 2.2) + nkf (~> 0.2.0) + optparse (>= 0.1.1, < 1.0.0) + ostruct (>= 0.1.0) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.5) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (~> 3) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.4.1) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) + fastlane-sirp (1.0.0) + sysrandom (~> 1.0) + gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.97.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-core (0.18.0) + addressable (~> 2.5, >= 2.5.1) + googleauth (~> 1.9) + httpclient (>= 2.8.3, < 3.a) + mini_mime (~> 1.0) + mutex_m + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + google-apis-iamcredentials_v1 (0.26.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-playcustomapp_v1 (0.17.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-storage_v1 (0.61.0) + google-apis-core (>= 0.15.0, < 2.a) + google-cloud-core (1.8.0) + google-cloud-env (>= 1.0, < 3.a) + google-cloud-errors (~> 1.0) + google-cloud-env (2.1.1) + faraday (>= 1.0, < 3.a) + google-cloud-errors (1.6.0) + google-cloud-storage (1.58.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-core (>= 0.18, < 2) + google-apis-iamcredentials_v1 (~> 0.18) + google-apis-storage_v1 (>= 0.42) + google-cloud-core (~> 1.6) + googleauth (~> 1.9) + mini_mime (~> 1.0) + googleauth (1.11.2) + faraday (>= 1.0, < 3.a) + google-cloud-env (~> 2.1) + jwt (>= 1.4, < 3.0) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.8) + domain_name (~> 0.5) + httpclient (2.9.0) + mutex_m + jmespath (1.6.2) + json (2.19.2) + jwt (2.10.2) + base64 + logger (1.7.0) + mini_magick (4.13.2) + mini_mime (1.1.5) + multi_json (1.19.1) + multipart-post (2.4.1) + mutex_m (0.3.0) + nanaimo (0.4.0) + naturally (2.3.0) + nkf (0.2.0) + optparse (0.8.1) + os (1.1.4) + ostruct (0.6.3) + plist (3.7.2) + public_suffix (7.0.5) + rake (13.3.1) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.4.1) + rexml (3.4.4) + rouge (3.28.0) + ruby2_keywords (0.0.5) + rubyzip (2.4.1) + security (0.1.5) + signet (0.21.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 4.0) + multi_json (~> 1.10) + simctl (1.6.10) + CFPropertyList + naturally + sysrandom (1.0.5) + terminal-notifier (2.0.0) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + trailblazer-option (0.1.2) + tty-cursor (0.7.1) + tty-screen (0.8.2) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + uber (0.1.0) + unicode-display_width (2.6.0) + word_wrap (1.0.0) + xcodeproj (1.27.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.4.0) + rexml (>= 3.3.6, < 4.0) + xcpretty (0.4.1) + rouge (~> 3.28.0) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + arm64-darwin-25 + ruby + +DEPENDENCIES + fastlane + +CHECKSUMS + CFPropertyList (3.0.8) + abbrev (0.1.2) + addressable (2.8.9) + artifactory (3.0.17) + atomos (0.1.3) + aws-eventstream (1.4.0) + aws-partitions (1.1229.0) + aws-sdk-core (3.244.0) + aws-sdk-kms (1.123.0) + aws-sdk-s3 (1.217.0) + aws-sigv4 (1.12.1) + babosa (1.0.4) + base64 (0.2.0) + benchmark (0.5.0) + bigdecimal (4.0.1) + claide (1.1.0) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + csv (3.3.5) + declarative (0.0.20) + digest-crc (0.7.0) + domain_name (0.6.20240107) + dotenv (2.8.1) + emoji_regex (3.2.3) + excon (0.112.0) + faraday (1.10.5) + faraday-cookie_jar (0.0.8) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.1) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.2.0) + faraday-net_http (1.0.2) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.4) + faraday_middleware (1.2.1) + fastimage (2.4.1) + fastlane (2.232.2) + fastlane-sirp (1.0.0) + gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.97.0) + google-apis-core (0.18.0) + google-apis-iamcredentials_v1 (0.26.0) + google-apis-playcustomapp_v1 (0.17.0) + google-apis-storage_v1 (0.61.0) + google-cloud-core (1.8.0) + google-cloud-env (2.1.1) + google-cloud-errors (1.6.0) + google-cloud-storage (1.58.0) + googleauth (1.11.2) + highline (2.0.3) + http-cookie (1.0.8) + httpclient (2.9.0) + jmespath (1.6.2) + json (2.19.2) + jwt (2.10.2) + logger (1.7.0) + mini_magick (4.13.2) + mini_mime (1.1.5) + multi_json (1.19.1) + multipart-post (2.4.1) + mutex_m (0.3.0) + nanaimo (0.4.0) + naturally (2.3.0) + nkf (0.2.0) + optparse (0.8.1) + os (1.1.4) + ostruct (0.6.3) + plist (3.7.2) + public_suffix (7.0.5) + rake (13.3.1) + representable (3.2.0) + retriable (3.4.1) + rexml (3.4.4) + rouge (3.28.0) + ruby2_keywords (0.0.5) + rubyzip (2.4.1) + security (0.1.5) + signet (0.21.0) + simctl (1.6.10) + sysrandom (1.0.5) + terminal-notifier (2.0.0) + terminal-table (3.0.2) + trailblazer-option (0.1.2) + tty-cursor (0.7.1) + tty-screen (0.8.2) + tty-spinner (0.9.3) + uber (0.1.0) + unicode-display_width (2.6.0) + word_wrap (1.0.0) + xcodeproj (1.27.0) + xcpretty (0.4.1) + xcpretty-travis-formatter (1.0.1) + +BUNDLED WITH + 4.0.8 diff --git a/MealMood.xcodeproj/project.pbxproj b/MealMood.xcodeproj/project.pbxproj index bf144fc..4034aef 100644 --- a/MealMood.xcodeproj/project.pbxproj +++ b/MealMood.xcodeproj/project.pbxproj @@ -83,6 +83,8 @@ FB0011111111111111111101 /* FirebaseAnalytics in Frameworks */ = {isa = PBXBuildFile; productRef = FB0011111111111111111105 /* FirebaseAnalytics */; }; FB0011111111111111111102 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = FB0011111111111111111103 /* GoogleService-Info.plist */; }; FB0011111111111111111106 /* AnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB0011111111111111111107 /* AnalyticsService.swift */; }; + FB0011111111111111111108 /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = FB001111111111111111110B /* FirebaseCrashlytics */; }; + FB0011111111111111111109 /* CrashlyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB001111111111111111110A /* CrashlyticsService.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -190,6 +192,7 @@ F766AA7898D6F391DE1B9A81 /* MealMoodTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MealMoodTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; FB0011111111111111111103 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; FB0011111111111111111107 /* AnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsService.swift; sourceTree = ""; }; + FB001111111111111111110A /* CrashlyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashlyticsService.swift; sourceTree = ""; }; FC01EE9D78D980727A644CDF /* View+MealCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+MealCard.swift"; sourceTree = ""; }; FD4FD72583C004A5D5C939A3 /* FilledSlotView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilledSlotView.swift; sourceTree = ""; }; FD621CE9B44512AE8894D92E /* OnboardingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewModel.swift; sourceTree = ""; }; @@ -214,6 +217,7 @@ files = ( 4D53B5101AE845D7C03A0EB4 /* GoogleMobileAds in Frameworks */, FB0011111111111111111101 /* FirebaseAnalytics in Frameworks */, + FB0011111111111111111108 /* FirebaseCrashlytics in Frameworks */, C3CB4ACC213DAAD25E610C56 /* WidgetKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -460,6 +464,7 @@ children = ( 812303C94C8C1D2FADF3CB37 /* AdMobConfig.swift */, FB0011111111111111111107 /* AnalyticsService.swift */, + FB001111111111111111110A /* CrashlyticsService.swift */, CE7AB2ECCA7D330E50AC549E /* AutocompleteEngine.swift */, B0A631C6D0CF7A0724ACF6A5 /* CalendarService.swift */, 17E00AB23342224B3407838B /* DefaultDataService.swift */, @@ -519,6 +524,7 @@ 60AC9519630B6008787A8859 /* Resources */, 9240A703A486116A9871C534 /* Frameworks */, DF9575A974376FF5C6420060 /* Embed App Extensions */, + FB001111111111111111110C /* Upload dSYM to Crashlytics */, ); buildRules = ( ); @@ -528,6 +534,7 @@ packageProductDependencies = ( 192E7CF40FD34C3FF4DDFDA3 /* GoogleMobileAds */, FB0011111111111111111105 /* FirebaseAnalytics */, + FB001111111111111111110B /* FirebaseCrashlytics */, ); productName = MealMood; productReference = C7B13F4D4D362E50CA65A0C1 /* MealMood.app */; @@ -677,11 +684,31 @@ 17E1054EE183486E7BE38C19 /* WidgetDataStore.swift in Sources */, 233B9C1A5CA7E938318491C0 /* SocialShareSheet.swift in Sources */, 25B3C328E088A403CABAF20A /* PremiumSyncService.swift in Sources */, + FB0011111111111111111109 /* CrashlyticsService.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + FB001111111111111111110C /* Upload dSYM to Crashlytics */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${TARGET_NAME}", + "$(SRCROOT)/$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)", + ); + name = "Upload dSYM to Crashlytics"; + outputFileListPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${BUILD_DIR%Build/*}SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run\"\n"; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXTargetDependency section */ C4E8F43173BDFCAE4D6DFF49 /* PBXTargetDependency */ = { isa = PBXTargetDependency; @@ -828,7 +855,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = MealMood/Resources/MealMood.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 21; + CURRENT_PROJECT_VERSION = 22; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = MealMood/Resources/Info.plist; @@ -837,7 +864,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 1.0.5; PRODUCT_BUNDLE_IDENTIFIER = com.alexandrevazquez.mealmood; PRODUCT_NAME = MealMood; SDKROOT = iphoneos; @@ -933,7 +960,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = MealMood/Resources/MealMood.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 21; + CURRENT_PROJECT_VERSION = 22; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = MealMood/Resources/Info.plist; @@ -942,7 +969,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 1.0.5; PRODUCT_BUNDLE_IDENTIFIER = com.alexandrevazquez.mealmood; PRODUCT_NAME = MealMood; SDKROOT = iphoneos; @@ -1021,6 +1048,11 @@ package = FB0011111111111111111104 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; productName = FirebaseAnalytics; }; + FB001111111111111111110B /* FirebaseCrashlytics */ = { + isa = XCSwiftPackageProductDependency; + package = FB0011111111111111111104 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseCrashlytics; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 7C11D794B7E45A145D5ADBFD /* Project object */; diff --git a/MealMood.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/MealMood.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index b48b7ba..86df026 100644 --- a/MealMood.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/MealMood.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,6 +1,123 @@ { - "originHash" : "3b9752ff554d5fdc91a7c01c2322a5d851b0979c36f02eb55188d9ab9e3d7e77", + "originHash" : "f485fb75b59651d1d5e1bfe15bfd2a2b6dd9737b87cd29eaaa02288efe4fe401", "pins" : [ + { + "identity" : "abseil-cpp-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/abseil-cpp-binary.git", + "state" : { + "revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5", + "version" : "1.2024072200.0" + } + }, + { + "identity" : "app-check", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/app-check.git", + "state" : { + "revision" : "61b85103a1aeed8218f17c794687781505fbbef5", + "version" : "11.2.0" + } + }, + { + "identity" : "firebase-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/firebase-ios-sdk.git", + "state" : { + "revision" : "fdc352fabaf5916e7faa1f96ad02b1957e93e5a5", + "version" : "11.15.0" + } + }, + { + "identity" : "google-ads-on-device-conversion-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk", + "state" : { + "revision" : "a2d0f1f1666de591eb1a811f40b1706f5c63a2ed", + "version" : "2.3.0" + } + }, + { + "identity" : "googleappmeasurement", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleAppMeasurement.git", + "state" : { + "revision" : "45ce435e9406d3c674dd249a042b932bee006f60", + "version" : "11.15.0" + } + }, + { + "identity" : "googledatatransport", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleDataTransport.git", + "state" : { + "revision" : "617af071af9aa1d6a091d59a202910ac482128f9", + "version" : "10.1.0" + } + }, + { + "identity" : "googleutilities", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleUtilities.git", + "state" : { + "revision" : "60da361632d0de02786f709bdc0c4df340f7613e", + "version" : "8.1.0" + } + }, + { + "identity" : "grpc-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/grpc-binary.git", + "state" : { + "revision" : "75b31c842f664a0f46a2e590a570e370249fd8f6", + "version" : "1.69.1" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "c756a29784521063b6a1202907e2cc47f41b667c", + "version" : "4.5.0" + } + }, + { + "identity" : "interop-ios-for-google-sdks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/interop-ios-for-google-sdks.git", + "state" : { + "revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe", + "version" : "101.0.0" + } + }, + { + "identity" : "leveldb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/leveldb.git", + "state" : { + "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1", + "version" : "1.22.5" + } + }, + { + "identity" : "nanopb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/nanopb.git", + "state" : { + "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1", + "version" : "2.30910.0" + } + }, + { + "identity" : "promises", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/promises.git", + "state" : { + "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", + "version" : "2.4.0" + } + }, { "identity" : "swift-package-manager-google-mobile-ads", "kind" : "remoteSourceControl", @@ -18,6 +135,15 @@ "revision" : "708a282840c2171ee63bd93b87afa49fe507d70e", "version" : "2.7.0" } + }, + { + "identity" : "swift-protobuf", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-protobuf.git", + "state" : { + "revision" : "5596511fce902e649c403cd4d6d5da1254f142b7", + "version" : "1.35.1" + } } ], "version" : 3 diff --git a/MealMood/Components/EmptySlotView.swift b/MealMood/Components/EmptySlotView.swift index 4471533..f41df1f 100644 --- a/MealMood/Components/EmptySlotView.swift +++ b/MealMood/Components/EmptySlotView.swift @@ -43,3 +43,31 @@ struct EmptySlotView: View { return valid ? .mealMoodSuccess : .mealMoodError } } + +struct EatingOutSlotView: View { + let mealType: MealType + + var body: some View { + VStack(spacing: 6) { + Image(systemName: "fork.knife.circle") + .font(.system(size: 20)) + .foregroundColor(Color(hex: "#A0C4B8")) + + Text("slot_eating_out") + .font(.mealMoodCaption) + .foregroundColor(Color(hex: "#6B9E90")) + } + .frame(maxWidth: .infinity, minHeight: 80) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(Color(hex: "#EEF8F5")) + .overlay( + RoundedRectangle(cornerRadius: 12) + .strokeBorder( + style: StrokeStyle(lineWidth: 2, dash: [5]) + ) + .foregroundColor(Color(hex: "#A0C4B8")) + ) + ) + } +} diff --git a/MealMood/ContentView.swift b/MealMood/ContentView.swift index 5258994..4780a8c 100644 --- a/MealMood/ContentView.swift +++ b/MealMood/ContentView.swift @@ -1,22 +1,20 @@ import SwiftUI import SwiftData +import StoreKit struct ContentView: View { @Environment(\.modelContext) private var context @Environment(\.scenePhase) private var scenePhase @Query private var allSettings: [AppSettings] @State private var isReady = false + @State private var hasCompletedColdLaunch = false + private let premiumSyncService = PremiumSyncService() private var settings: AppSettings? { allSettings.first } - private var isTestFlightBuild: Bool { - guard let receiptURL = Bundle.main.appStoreReceiptURL else { return false } - return receiptURL.lastPathComponent == "sandboxReceipt" - } var body: some View { Group { if !isReady { - // Splash screen ZStack { Color.mealMoodBackground.ignoresSafeArea() AppIconPlaceholder(size: 120) @@ -30,13 +28,39 @@ struct ContentView: View { } } .environment(\.locale, Locale(identifier: settings?.languageEnum.localeIdentifier ?? Locale.current.identifier)) + // MARK: Transaction.updates listener (StoreKit 2 recommended pattern) + // Fires for new purchases, renewals and revocations while the app is running. + // This is the most reliable way to catch a purchase even if the app was + // interrupted during the payment flow. + .task { + for await result in Transaction.updates { + guard case .verified(let transaction) = result else { continue } + guard let s = settings else { + await transaction.finish() + continue + } + let isActive = transaction.revocationDate == nil && + (transaction.expirationDate.map { $0 > Date() } ?? true) + + if isActive && !s.isPremium { + s.isPremium = true + try? context.save() + } else if transaction.revocationDate != nil && s.isPremium { + // Explicitly revoked subscription → downgrade + s.isPremium = false + try? context.save() + } + await transaction.finish() + } + } .onAppear { initializeAppIfNeeded() Task { if settings?.iCloudSyncEnabledResolved ?? true { await ICloudSyncService.shared.pullRemoteIfNeeded(context: context) } - await syncPremiumStatus() + await syncPremiumStatus(isColdLaunch: true) + hasCompletedColdLaunch = true } DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { withAnimation(.easeInOut) { @@ -48,7 +72,11 @@ struct ContentView: View { Task { switch phase { case .active: - await syncPremiumStatus() + // Only run foreground sync after cold launch is complete. + // The first activation is covered by .onAppear above. + if hasCompletedColdLaunch { + await syncPremiumStatus(isColdLaunch: false) + } case .inactive, .background: if settings?.iCloudSyncEnabledResolved ?? true { await ICloudSyncService.shared.pushLocalSnapshot(context: context) @@ -60,35 +88,28 @@ struct ContentView: View { } } + // MARK: - Private + private func initializeAppIfNeeded() { - // Create default settings if none exist if allSettings.isEmpty { DefaultDataService.createDefaultSettings(context: context) } - - // Create default tags if none exist let tagDescriptor = FetchDescriptor() if (try? context.fetch(tagDescriptor))?.isEmpty ?? true { DefaultDataService.createDefaultTags(context: context) } } - private func syncPremiumStatus() async { - let descriptor = FetchDescriptor() - guard let settings = try? context.fetch(descriptor).first else { return } - - if isTestFlightBuild { - if settings.isPremium != true { - settings.isPremium = true - try? context.save() - } - return - } - - let premium = await StoreManager.hasActiveSubscription() - if settings.isPremium != premium { - settings.isPremium = premium + private func syncPremiumStatus(isColdLaunch: Bool) async { + guard let settings else { return } + let newPremium = await premiumSyncService.sync( + currentIsPremium: settings.isPremium, + isColdLaunch: isColdLaunch + ) + if settings.isPremium != newPremium { + settings.isPremium = newPremium try? context.save() } + CrashlyticsService.setUserProperties(isPremium: settings.isPremium) } } diff --git a/MealMood/MealMoodApp.swift b/MealMood/MealMoodApp.swift index c0a73f8..3ef2a30 100644 --- a/MealMood/MealMoodApp.swift +++ b/MealMood/MealMoodApp.swift @@ -1,5 +1,7 @@ import SwiftUI import SwiftData +import FirebaseCore +import FirebaseCrashlytics #if canImport(GoogleMobileAds) import GoogleMobileAds #endif @@ -19,6 +21,7 @@ struct MealMoodApp: App { }() init() { + FirebaseApp.configure() #if canImport(GoogleMobileAds) GADMobileAds.sharedInstance().start(completionHandler: nil) #endif diff --git a/MealMood/Models/MealSlot.swift b/MealMood/Models/MealSlot.swift index 40c7791..daa21b9 100644 --- a/MealMood/Models/MealSlot.swift +++ b/MealMood/Models/MealSlot.swift @@ -9,6 +9,7 @@ final class MealSlot { var dishId: UUID? var calendarEventId: String? var isRuleOverridden: Bool + var isEatingOut: Bool var weekPlan: WeekPlan? @@ -18,7 +19,8 @@ final class MealSlot { mealType: String, dishId: UUID? = nil, calendarEventId: String? = nil, - isRuleOverridden: Bool = false + isRuleOverridden: Bool = false, + isEatingOut: Bool = false ) { self.id = id self.dayOfWeek = dayOfWeek @@ -26,6 +28,7 @@ final class MealSlot { self.dishId = dishId self.calendarEventId = calendarEventId self.isRuleOverridden = isRuleOverridden + self.isEatingOut = isEatingOut } var mealTypeEnum: MealType { diff --git a/MealMood/Resources/GoogleService-Info.plist b/MealMood/Resources/GoogleService-Info.plist new file mode 100644 index 0000000..dfdb7e7 --- /dev/null +++ b/MealMood/Resources/GoogleService-Info.plist @@ -0,0 +1,30 @@ + + + + + API_KEY + AIzaSyBkDgBKsgudAAwsWErzLviIWm3dir4bHCc + GCM_SENDER_ID + 442954975324 + PLIST_VERSION + 1 + BUNDLE_ID + com.alexandrevazquez.mealmood + PROJECT_ID + mealmood + STORAGE_BUCKET + mealmood.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:442954975324:ios:f6fd3a3bf28b6f3d59be99 + + \ No newline at end of file diff --git a/MealMood/Resources/Info.plist b/MealMood/Resources/Info.plist index bc5d7e3..5ec24b1 100644 --- a/MealMood/Resources/Info.plist +++ b/MealMood/Resources/Info.plist @@ -15,9 +15,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0.4 + 1.0.5 CFBundleVersion - 21 + 22 GADApplicationIdentifier ca-app-pub-1549720748100858~9985112590 LSRequiresIPhoneOS diff --git a/MealMood/Resources/MealMood.entitlements b/MealMood/Resources/MealMood.entitlements index cf687e0..c04f40f 100644 --- a/MealMood/Resources/MealMood.entitlements +++ b/MealMood/Resources/MealMood.entitlements @@ -8,5 +8,9 @@ com.apple.developer.ubiquity-kvstore-identifier $(TeamIdentifierPrefix)$(CFBundleIdentifier) + com.apple.security.application-groups + + group.com.alexandrevazquez.mealmood + diff --git a/MealMood/Resources/de.lproj/Localizable.strings b/MealMood/Resources/de.lproj/Localizable.strings index 9b70cb8..a13b378 100644 --- a/MealMood/Resources/de.lproj/Localizable.strings +++ b/MealMood/Resources/de.lproj/Localizable.strings @@ -298,3 +298,15 @@ // Onboarding preview "onboarding_welcome_preview_label" = "So könnte deine Woche aussehen ✨"; + +// Auswärts essen +"slot_eating_out" = "Auswärts"; +"home_mark_eating_out" = "Auswärts essen"; +"toast_eating_out_marked" = "Als auswärts markiert"; + +// Regelkonflikte +"violations_panel_title" = "Regelkonflikte"; +"violations_panel_empty" = "Keine Konflikte diese Woche"; +"violations_fix" = "Slot leeren"; +"violations_ignore" = "Ignorieren"; +"violations_badge" = "%d Konflikt(e)"; diff --git a/MealMood/Resources/en.lproj/Localizable.strings b/MealMood/Resources/en.lproj/Localizable.strings index e4ec220..a8023d3 100644 --- a/MealMood/Resources/en.lproj/Localizable.strings +++ b/MealMood/Resources/en.lproj/Localizable.strings @@ -298,3 +298,15 @@ // Onboarding preview "onboarding_welcome_preview_label" = "Your week could look like this ✨"; + +// Eating out +"slot_eating_out" = "Eating out"; +"home_mark_eating_out" = "Mark as eating out"; +"toast_eating_out_marked" = "Marked as eating out"; + +// Rule violations panel +"violations_panel_title" = "Rule conflicts"; +"violations_panel_empty" = "No rule conflicts this week"; +"violations_fix" = "Clear slot"; +"violations_ignore" = "Ignore"; +"violations_badge" = "%d conflict(s)"; diff --git a/MealMood/Resources/es.lproj/Localizable.strings b/MealMood/Resources/es.lproj/Localizable.strings index 554484a..ea72431 100644 --- a/MealMood/Resources/es.lproj/Localizable.strings +++ b/MealMood/Resources/es.lproj/Localizable.strings @@ -298,3 +298,15 @@ // Onboarding preview "onboarding_welcome_preview_label" = "Tu semana puede quedar así ✨"; + +// Comer fuera +"slot_eating_out" = "Comiendo fuera"; +"home_mark_eating_out" = "Marcar como comida fuera"; +"toast_eating_out_marked" = "Marcado como comida fuera"; + +// Panel de conflictos +"violations_panel_title" = "Conflictos de reglas"; +"violations_panel_empty" = "Sin conflictos esta semana"; +"violations_fix" = "Vaciar hueco"; +"violations_ignore" = "Ignorar"; +"violations_badge" = "%d conflicto(s)"; diff --git a/MealMood/Resources/fr.lproj/Localizable.strings b/MealMood/Resources/fr.lproj/Localizable.strings index fdfd876..c77d3fd 100644 --- a/MealMood/Resources/fr.lproj/Localizable.strings +++ b/MealMood/Resources/fr.lproj/Localizable.strings @@ -298,3 +298,15 @@ // Onboarding preview "onboarding_welcome_preview_label" = "Votre semaine pourrait ressembler à ça ✨"; + +// Repas à l'extérieur +"slot_eating_out" = "Repas dehors"; +"home_mark_eating_out" = "Manger à l'extérieur"; +"toast_eating_out_marked" = "Repas marqué à l'extérieur"; + +// Panneau de conflits +"violations_panel_title" = "Conflits de règles"; +"violations_panel_empty" = "Aucun conflit cette semaine"; +"violations_fix" = "Vider le créneau"; +"violations_ignore" = "Ignorer"; +"violations_badge" = "%d conflit(s)"; diff --git a/MealMood/Resources/it.lproj/Localizable.strings b/MealMood/Resources/it.lproj/Localizable.strings index 20adf17..9aa0a53 100644 --- a/MealMood/Resources/it.lproj/Localizable.strings +++ b/MealMood/Resources/it.lproj/Localizable.strings @@ -298,3 +298,15 @@ // Onboarding preview "onboarding_welcome_preview_label" = "La tua settimana potrebbe essere così ✨"; + +// Mangiare fuori +"slot_eating_out" = "Fuori casa"; +"home_mark_eating_out" = "Mangiare fuori"; +"toast_eating_out_marked" = "Segnato come fuori casa"; + +// Pannello conflitti +"violations_panel_title" = "Conflitti di regole"; +"violations_panel_empty" = "Nessun conflitto questa settimana"; +"violations_fix" = "Svuota slot"; +"violations_ignore" = "Ignora"; +"violations_badge" = "%d conflitto/i"; diff --git a/MealMood/Resources/pt-BR.lproj/Localizable.strings b/MealMood/Resources/pt-BR.lproj/Localizable.strings index ed9764b..139de64 100644 --- a/MealMood/Resources/pt-BR.lproj/Localizable.strings +++ b/MealMood/Resources/pt-BR.lproj/Localizable.strings @@ -298,3 +298,15 @@ // Onboarding preview "onboarding_welcome_preview_label" = "Sua semana pode ficar assim ✨"; + +// Comer fora +"slot_eating_out" = "Comendo fora"; +"home_mark_eating_out" = "Comer fora"; +"toast_eating_out_marked" = "Marcado como comer fora"; + +// Painel de conflitos +"violations_panel_title" = "Conflitos de regras"; +"violations_panel_empty" = "Sem conflitos esta semana"; +"violations_fix" = "Limpar slot"; +"violations_ignore" = "Ignorar"; +"violations_badge" = "%d conflito(s)"; diff --git a/MealMood/Services/AutocompleteEngine.swift b/MealMood/Services/AutocompleteEngine.swift index dee011c..90215b1 100644 --- a/MealMood/Services/AutocompleteEngine.swift +++ b/MealMood/Services/AutocompleteEngine.swift @@ -7,6 +7,14 @@ struct AutocompleteEngine { let unfilledCount: Int } + struct RuleViolation { + let slotId: UUID + let dayOfWeek: Int + let mealType: String + let dishId: UUID + let dishName: String + } + static func autocomplete( emptySlots: [MealSlot], currentPlan: WeekPlan, @@ -18,9 +26,9 @@ struct AutocompleteEngine { var filledSlots: [(slotId: UUID, dishId: UUID)] = [] var unfilledCount = 0 - let sortedEmpty = emptySlots.sorted { - ($0.dayOfWeek, $0.mealType) < ($1.dayOfWeek, $1.mealType) - } + let sortedEmpty = emptySlots + .filter { !$0.isEatingOut } + .sorted { ($0.dayOfWeek, $0.mealType) < ($1.dayOfWeek, $1.mealType) } for slot in sortedEmpty { let strictCandidates = allDishes.filter { dish in @@ -66,6 +74,24 @@ struct AutocompleteEngine { return AutocompleteResult(filledSlots: filledSlots, unfilledCount: unfilledCount) } + static func findViolations(plan: WeekPlan, allDishes: [Dish], allTags: [Tag]) -> [RuleViolation] { + let tagMap = Dictionary(uniqueKeysWithValues: allTags.map { ($0.id, $0) }) + let dishMap = Dictionary(uniqueKeysWithValues: allDishes.map { ($0.id, $0) }) + return plan.slots.compactMap { slot in + guard let dishId = slot.dishId, + let dish = dishMap[dishId], + slot.isRuleOverridden, + !slot.isEatingOut else { return nil } + return RuleViolation( + slotId: slot.id, + dayOfWeek: slot.dayOfWeek, + mealType: slot.mealType, + dishId: dishId, + dishName: dish.name + ) + } + } + static func validateDrop( dish: Dish, slot: MealSlot, diff --git a/MealMood/Services/CrashlyticsService.swift b/MealMood/Services/CrashlyticsService.swift new file mode 100644 index 0000000..c4e6913 --- /dev/null +++ b/MealMood/Services/CrashlyticsService.swift @@ -0,0 +1,26 @@ +import FirebaseCrashlytics + +enum CrashlyticsService { + + static func setUserProperties(isPremium: Bool) { + Crashlytics.crashlytics().setCustomValue(isPremium, forKey: "is_premium") + } + + static func log(_ message: String) { + Crashlytics.crashlytics().log(message) + } + + static func record(_ error: Error, context: String? = nil) { + var userInfo: [String: Any] = [:] + if let context { userInfo["context"] = context } + Crashlytics.crashlytics().record(error: error, userInfo: userInfo) + } + + static func record(message: String, context: String? = nil) { + let userInfo: [String: Any] = context.map { ["context": $0] } ?? [:] + Crashlytics.crashlytics().record( + exceptionModel: ExceptionModel(name: "AppError", reason: message), + userInfo: userInfo + ) + } +} diff --git a/MealMood/Services/ICloudSyncService.swift b/MealMood/Services/ICloudSyncService.swift index 4b97a59..5511c78 100644 --- a/MealMood/Services/ICloudSyncService.swift +++ b/MealMood/Services/ICloudSyncService.swift @@ -71,7 +71,6 @@ final class ICloudSyncService { reminderMinutesBefore: $0.reminderMinutesBefore, iCloudSyncEnabled: $0.iCloudSyncEnabled, weekExportStyle: $0.weekExportStyle, - isPremium: $0.isPremium, onboardingCompleted: $0.onboardingCompleted ) }, @@ -153,7 +152,9 @@ final class ICloudSyncService { settings.reminderMinutesBefore = settingsPayload.reminderMinutesBefore settings.iCloudSyncEnabled = settingsPayload.iCloudSyncEnabled settings.weekExportStyle = settingsPayload.weekExportStyle - settings.isPremium = settingsPayload.isPremium + // isPremium is intentionally NOT synced via iCloud. + // It is determined exclusively by StoreKit to avoid stale state + // overwriting a freshly completed purchase. settings.onboardingCompleted = settingsPayload.onboardingCompleted context.insert(settings) } @@ -236,8 +237,9 @@ private struct SettingsPayload: Codable { let reminderMinutesBefore: Int? let iCloudSyncEnabled: Bool? let weekExportStyle: String? - let isPremium: Bool let onboardingCompleted: Bool + // isPremium is intentionally omitted — determined by StoreKit only, never by sync. + // Old payloads in iCloud may contain this key; it is safely ignored by the decoder. } private struct TagPayload: Codable { diff --git a/MealMood/Services/PremiumSyncService.swift b/MealMood/Services/PremiumSyncService.swift new file mode 100644 index 0000000..dfb3121 --- /dev/null +++ b/MealMood/Services/PremiumSyncService.swift @@ -0,0 +1,36 @@ +import Foundation + +/// Encapsulates the premium status sync state machine. +/// Extracted from ContentView to be independently testable. +@MainActor +final class PremiumSyncService { + + /// Injectable for testing — defaults to real StoreKit check. + var hasActiveSubscription: () async -> Bool = { + await StoreManager.hasActiveSubscription() + } + + /// Determine whether the user should be premium after a sync. + /// + /// Rules: + /// - Active subscription found → always return `true` (upgrade). + /// - No subscription, currently free → return `false`. + /// - No subscription, currently premium, **cold launch** → return `false` + /// (subscription likely expired between sessions). + /// - No subscription, currently premium, **foreground activation** → return `true` + /// (StoreKit `currentEntitlements` can return empty for a few seconds immediately + /// after a purchase while the receipt propagates; never downgrade mid-session). + func sync(currentIsPremium: Bool, isColdLaunch: Bool) async -> Bool { + let active = await hasActiveSubscription() + switch (active, currentIsPremium, isColdLaunch) { + case (true, _, _): + return true + case (false, false, _): + return false + case (false, true, true): + return false // Subscription expired → downgrade on cold launch + case (false, true, false): + return true // Keep premium on foreground (timing protection) + } + } +} diff --git a/MealMood/Services/StoreManager.swift b/MealMood/Services/StoreManager.swift index 18b2042..44b5c8f 100644 --- a/MealMood/Services/StoreManager.swift +++ b/MealMood/Services/StoreManager.swift @@ -77,6 +77,7 @@ final class StoreManager: ObservableObject { if case .verified(let transaction) = verification { await transaction.finish() isPremium = true + AnalyticsService.logPremiumPurchased() return .success } return .failed @@ -99,14 +100,18 @@ final class StoreManager: ObservableObject { do { try await AppStore.sync() + let wasNotPremium = !isPremium await checkPremiumStatus() + if wasNotPremium && isPremium { + AnalyticsService.logPremiumRestored() + } } catch { print("Restore failed: \(error)") } } private func checkPremiumStatus() async { - isPremium = await Self.hasActiveSubscription(productIds: productIds) + isPremium = await Self.hasActiveSubscription() } var monthlyProduct: Product? { @@ -133,12 +138,18 @@ final class StoreManager: ObservableObject { var debugProductIds: [String] { productIds } - static func hasActiveSubscription(productIds: [String] = [ - StoreManager.monthlyProductId - ]) async -> Bool { + static func hasActiveSubscription() async -> Bool { + // Check all known product ID variants to handle both the canonical ID + // and any bundle-prefixed IDs that may have been used at purchase time. + var knownIds: [String] = [monthlyProductId] + if let bundleId = Bundle.main.bundleIdentifier { + knownIds.append("\(bundleId).premium.monthly") + } + let ids = Set(knownIds) + for await result in Transaction.currentEntitlements { if case .verified(let transaction) = result, - productIds.contains(transaction.productID), + ids.contains(transaction.productID), transaction.revocationDate == nil { if let expirationDate = transaction.expirationDate, expirationDate < Date() { continue diff --git a/MealMood/Services/WidgetDataStore.swift b/MealMood/Services/WidgetDataStore.swift new file mode 100644 index 0000000..2475a2e --- /dev/null +++ b/MealMood/Services/WidgetDataStore.swift @@ -0,0 +1,55 @@ +import Foundation +import WidgetKit + +struct TodayMealData: Codable { + let lunch: String? + let dinner: String? + let lunchLabel: String + let dinnerLabel: String + let weekdayName: String + let updatedAt: Date +} + +enum WidgetDataStore { + static let appGroupID = "group.com.alexandrevazquez.mealmood" + private static let key = "mealmood.today_meals" + + static func update(plan: WeekPlan?, dishes: [Dish], settings: AppSettings) { + let today = Date() + let weekday = Calendar.current.component(.weekday, from: today) + // weekday: 1=Sun,2=Mon..7=Sat → appDayOfWeek: 0=Mon..6=Sun + let appDayOfWeek = (weekday + 5) % 7 + + let lunchId = plan?.slots.first { $0.dayOfWeek == appDayOfWeek && $0.mealType == MealType.lunch.rawValue }?.dishId + let dinnerId = plan?.slots.first { $0.dayOfWeek == appDayOfWeek && $0.mealType == MealType.dinner.rawValue }?.dishId + + let lunch = lunchId.flatMap { id in dishes.first { $0.id == id }?.name } + let dinner = dinnerId.flatMap { id in dishes.first { $0.id == id }?.name } + + let locale = Locale(identifier: settings.languageEnum.resolved().localeIdentifier) + let formatter = DateFormatter() + formatter.locale = locale + formatter.dateFormat = "EEEE" + let weekdayName = formatter.string(from: today).capitalized(with: locale) + + let data = TodayMealData( + lunch: lunch, + dinner: dinner, + lunchLabel: String(localized: "lunch"), + dinnerLabel: String(localized: "dinner"), + weekdayName: weekdayName, + updatedAt: today + ) + + guard let defaults = UserDefaults(suiteName: appGroupID), + let encoded = try? JSONEncoder().encode(data) else { return } + defaults.set(encoded, forKey: key) + WidgetCenter.shared.reloadAllTimelines() + } + + static func read() -> TodayMealData? { + guard let defaults = UserDefaults(suiteName: appGroupID), + let data = defaults.data(forKey: key) else { return nil } + return try? JSONDecoder().decode(TodayMealData.self, from: data) + } +} diff --git a/MealMood/ViewModels/HomeViewModel.swift b/MealMood/ViewModels/HomeViewModel.swift index 5bd47e2..45b9ac8 100644 --- a/MealMood/ViewModels/HomeViewModel.swift +++ b/MealMood/ViewModels/HomeViewModel.swift @@ -12,6 +12,7 @@ final class HomeViewModel: ObservableObject { private struct SlotSnapshot { let dishId: UUID? let isRuleOverridden: Bool + let isEatingOut: Bool } @Published var currentWeekStart: Date @@ -116,7 +117,7 @@ final class HomeViewModel: ObservableObject { captureUndoSnapshot(plan: plan) isAutoCompleting = true - let emptySlots = plan.slots.filter { $0.dishId == nil } + let emptySlots = plan.slots.filter { $0.dishId == nil && !$0.isEatingOut } let result = AutocompleteEngine.autocomplete( emptySlots: emptySlots, currentPlan: plan, @@ -160,6 +161,7 @@ final class HomeViewModel: ObservableObject { } slot.dishId = nil slot.isRuleOverridden = false + slot.isEatingOut = false } plan.updatedAt = Date() AnalyticsService.logWeekReset() @@ -297,9 +299,11 @@ final class HomeViewModel: ObservableObject { if let snap = lastSlotsSnapshot[slot.id] { slot.dishId = snap.dishId slot.isRuleOverridden = snap.isRuleOverridden + slot.isEatingOut = snap.isEatingOut } else { slot.dishId = nil slot.isRuleOverridden = false + slot.isEatingOut = false } } @@ -323,7 +327,7 @@ final class HomeViewModel: ObservableObject { guard !isApplyingUndo else { return } lastWeekStartSnapshot = plan.weekStartDate lastSlotsSnapshot = Dictionary(uniqueKeysWithValues: plan.slots.map { slot in - (slot.id, SlotSnapshot(dishId: slot.dishId, isRuleOverridden: slot.isRuleOverridden)) + (slot.id, SlotSnapshot(dishId: slot.dishId, isRuleOverridden: slot.isRuleOverridden, isEatingOut: slot.isEatingOut)) }) hasUndoSnapshot = true } @@ -336,7 +340,7 @@ final class HomeViewModel: ObservableObject { private func firstFreeSlot(in plan: WeekPlan) -> MealSlot? { plan.slots - .filter { $0.dishId == nil } + .filter { $0.dishId == nil && !$0.isEatingOut } .sorted { lhs, rhs in if lhs.dayOfWeek != rhs.dayOfWeek { return lhs.dayOfWeek < rhs.dayOfWeek @@ -355,7 +359,7 @@ final class HomeViewModel: ObservableObject { switch settings.syncModeEnum { case .weekComplete: - if plan.slots.allSatisfy({ $0.dishId != nil }) { + if plan.slots.allSatisfy({ $0.dishId != nil || $0.isEatingOut }) { let descriptor = FetchDescriptor() let dishes = (try? plan.modelContext?.fetch(descriptor)) ?? [] syncAllAssignedSlotsToCalendar(plan: plan, dishes: dishes, settings: settings) @@ -426,6 +430,33 @@ final class HomeViewModel: ObservableObject { slot.isRuleOverridden = !valid } + func markEatingOut(slot: MealSlot, plan: WeekPlan, settings: AppSettings) { + captureUndoSnapshot(plan: plan) + if let eventId = slot.calendarEventId { + CalendarService.shared.deleteEvent(eventId: eventId) + slot.calendarEventId = nil + } + slot.dishId = nil + slot.isRuleOverridden = false + slot.isEatingOut = true + plan.updatedAt = Date() + applyCalendarSyncPolicy(plan: plan, settings: settings, shouldNotify: false) + HapticManager.shared.impact(style: .medium) + showToastMessage(localizedString("toast_eating_out_marked", language: settings.languageEnum.resolved())) + } + + func unmarkEatingOut(slot: MealSlot, plan: WeekPlan, settings: AppSettings) { + captureUndoSnapshot(plan: plan) + slot.isEatingOut = false + plan.updatedAt = Date() + HapticManager.shared.impact(style: .light) + } + + func acknowledgeViolation(slot: MealSlot, plan: WeekPlan) { + slot.isRuleOverridden = false + plan.updatedAt = Date() + } + func confirmInvalidDrop(_ dish: Dish, to slot: MealSlot, plan: WeekPlan, settings: AppSettings) { invalidDropContext = InvalidDropContext(dish: dish, slot: slot) HapticManager.shared.notification(type: .warning) diff --git a/MealMood/Views/Home/DishDrawerView.swift b/MealMood/Views/Home/DishDrawerView.swift index 3e797f6..623ad29 100644 --- a/MealMood/Views/Home/DishDrawerView.swift +++ b/MealMood/Views/Home/DishDrawerView.swift @@ -62,6 +62,8 @@ struct DishDrawerView: View { .foregroundColor(.mealMoodTextSecondary) TextField("home_my_dishes_search", text: $searchText) .font(.mealMoodBody) + .foregroundColor(.mealMoodTextPrimary) + .tint(.mealMoodCoral) if !searchText.isEmpty { Button { searchText = "" diff --git a/MealMood/Views/Home/HomeView.swift b/MealMood/Views/Home/HomeView.swift index 6f93230..b688fe9 100644 --- a/MealMood/Views/Home/HomeView.swift +++ b/MealMood/Views/Home/HomeView.swift @@ -29,8 +29,11 @@ struct HomeView: View { @State private var pendingExportPlan: WeekPlan? @State private var shareImageURL: URL? @State private var showShareSheet: Bool = false + @State private var showViolationsPanel: Bool = false private var settings: AppSettings? { allSettings.first } + private var isRunningOnMac: Bool { ProcessInfo.processInfo.isiOSAppOnMac } + private var contentHorizontalPadding: CGFloat { (isRunningOnMac || horizontalSizeClass == .regular) ? 14 : 0 } var body: some View { NavigationStack { @@ -140,6 +143,18 @@ struct HomeView: View { Label("home_copy_previous_week", systemImage: "doc.on.doc") } + let violationCount = AutocompleteEngine.findViolations(plan: plan, allDishes: dishes, allTags: tags).count + if violationCount > 0 { + Button { + showViolationsPanel = true + } label: { + Label( + String(format: String(localized: "violations_badge"), violationCount), + systemImage: "exclamationmark.triangle.fill" + ) + } + } + Button(role: .destructive) { viewModel.showResetAlert = true } label: { @@ -165,6 +180,7 @@ struct HomeView: View { } } .onAppear { + AnalyticsService.logScreenView("Home") guard let settings = settings, let plan = ensureCurrentWeekPlanExists(settings: settings) else { return } viewModel.reconcileSlotsIfNeeded(plan: plan, settings: settings, context: context) @@ -222,14 +238,14 @@ struct HomeView: View { } .frame(maxHeight: .infinity) } - .frame(maxWidth: horizontalSizeClass == .regular ? 1160 : .infinity) + .frame(maxWidth: .infinity) .frame(maxWidth: .infinity, alignment: .top) - .padding(.horizontal, horizontalSizeClass == .regular ? 14 : 0) + .padding(.horizontal, contentHorizontalPadding) .padding(.bottom, 0) } .frame(maxHeight: .infinity, alignment: .top) .safeAreaInset(edge: .bottom, spacing: 0) { - if !settings.isPremium { + if !settings.isPremium && !isRunningOnMac { AdBannerView() } } @@ -366,7 +382,6 @@ struct HomeView: View { if isValid { viewModel.assignDish(dish, to: freshSlot, plan: plan, settings: settings) } else { - // In picker flow, assign anyway and mark as override so the action is never lost. viewModel.assignDish(dish, to: freshSlot, plan: plan, settings: settings, isOverride: true) } selectedEmptySlotId = nil @@ -376,6 +391,14 @@ struct HomeView: View { DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { viewModel.showDishForm = true } + }, + onEatingOut: { + guard let freshSlot = plan.slots.first(where: { $0.id == slotId }) else { + selectedEmptySlotId = nil + return + } + selectedEmptySlotId = nil + viewModel.markEatingOut(slot: freshSlot, plan: plan, settings: settings) } ) } @@ -405,9 +428,23 @@ struct HomeView: View { } .sheet(isPresented: $showShareSheet) { if let shareImageURL { - ShareSheet(activityItems: [shareImageURL]) + SocialShareSheet(imageURL: shareImageURL) } } + .sheet(isPresented: $showViolationsPanel) { + RuleViolationsPanelSheet( + plan: plan, + dishes: dishes, + tags: tags, + settings: settings, + onFix: { slot in + viewModel.removeDish(from: slot, plan: plan, settings: settings) + }, + onIgnore: { slot in + viewModel.acknowledgeViolation(slot: slot, plan: plan) + } + ) + } .sheet(isPresented: $showMonthlyHistory) { MonthlyHistoryView( weekPlans: weekPlans, @@ -427,6 +464,7 @@ struct HomeView: View { ) wasWeekComplete = isWeekComplete(plan: plan) evaluatePostOnboardingPromptsIfNeeded(plan: plan, settings: settings) + updateWidget(settings: settings) } .onChange(of: plan.updatedAt) { _, _ in let nowComplete = isWeekComplete(plan: plan) @@ -434,6 +472,7 @@ struct HomeView: View { evaluateReviewPrompt() } wasWeekComplete = nowComplete + updateWidget(settings: settings) } } @@ -442,6 +481,7 @@ struct HomeView: View { let tags: [Tag] let onPickDish: (Dish) -> Void let onCreateDish: () -> Void + var onEatingOut: (() -> Void)? = nil @Environment(\.dismiss) private var dismiss @State private var searchText: String = "" @@ -477,6 +517,18 @@ struct HomeView: View { .padding(24) } else { List { + if let onEatingOut, searchText.isEmpty { + Button { + dismiss() + onEatingOut() + } label: { + Label("home_mark_eating_out", systemImage: "fork.knife.circle") + .font(.mealMoodBody) + .foregroundColor(Color(hex: "#6B9E90")) + } + .listRowBackground(Color(hex: "#EEF8F5")) + } + ForEach(filteredDishes, id: \.id) { dish in Button { onPickDish(dish) @@ -596,7 +648,12 @@ struct HomeView: View { } private func isWeekComplete(plan: WeekPlan) -> Bool { - plan.slots.allSatisfy { $0.dishId != nil } + plan.slots.allSatisfy { $0.dishId != nil || $0.isEatingOut } + } + + private func updateWidget(settings: AppSettings) { + let todayPlan = fetchWeekPlan(for: Date().startOfWeek()) + WidgetDataStore.update(plan: todayPlan, dishes: dishes, settings: settings) } @ViewBuilder @@ -678,6 +735,7 @@ struct HomeView: View { } shareImageURL = url showShareSheet = true + AnalyticsService.logWeekPlanShared(format: settings.weekExportStyleEnum.rawValue) } private func renderWeekShareImage(plan: WeekPlan, settings: AppSettings) -> UIImage? { @@ -777,6 +835,135 @@ struct HomeView: View { } } +private struct RuleViolationsPanelSheet: View { + let plan: WeekPlan + let dishes: [Dish] + let tags: [Tag] + let settings: AppSettings + let onFix: (MealSlot) -> Void + let onIgnore: (MealSlot) -> Void + + @Environment(\.dismiss) private var dismiss + + private var violations: [AutocompleteEngine.RuleViolation] { + AutocompleteEngine.findViolations(plan: plan, allDishes: dishes, allTags: tags) + } + + var body: some View { + NavigationStack { + Group { + if violations.isEmpty { + VStack(spacing: 12) { + Image(systemName: "checkmark.circle.fill") + .font(.system(size: 44)) + .foregroundColor(.mealMoodSuccess) + Text("violations_panel_empty") + .font(.mealMoodBody) + .foregroundColor(.mealMoodTextSecondary) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .padding(24) + } else { + List { + ForEach(violations, id: \.slotId) { violation in + if let slot = plan.slots.first(where: { $0.id == violation.slotId }) { + ViolationRow( + violation: violation, + settings: settings, + onFix: { + onFix(slot) + if violations.count <= 1 { dismiss() } + }, + onIgnore: { + onIgnore(slot) + if violations.count <= 1 { dismiss() } + } + ) + .listRowBackground(Color.mealMoodSurface) + } + } + } + .listStyle(.plain) + .scrollContentBackground(.hidden) + } + } + .background(Color.mealMoodBackground.ignoresSafeArea()) + .navigationTitle("violations_panel_title") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("tag_selector_done") { dismiss() } + } + } + } + } + + private struct ViolationRow: View { + let violation: AutocompleteEngine.RuleViolation + let settings: AppSettings + let onFix: () -> Void + let onIgnore: () -> Void + + private var dayLabel: String { + let language = settings.languageEnum.resolved() + return localizedString(dayKey(for: violation.dayOfWeek), language: language) + } + + private var mealLabel: String { + let language = settings.languageEnum.resolved() + return localizedString(violation.mealType, language: language) + } + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + HStack(spacing: 6) { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundColor(.mealMoodWarning) + .font(.system(size: 14)) + Text("\(dayLabel) · \(mealLabel)") + .font(.mealMoodSmall) + .foregroundColor(.mealMoodTextSecondary) + } + + Text(violation.dishName) + .font(.mealMoodBodyBold) + .foregroundColor(.mealMoodTextPrimary) + + HStack(spacing: 10) { + Button(action: onFix) { + Text("violations_fix") + .font(.mealMoodSmall) + .foregroundColor(.white) + .padding(.horizontal, 14) + .padding(.vertical, 6) + .background(Color.mealMoodCoral) + .clipShape(Capsule()) + } + .buttonStyle(.plain) + + Button(action: onIgnore) { + Text("violations_ignore") + .font(.mealMoodSmall) + .foregroundColor(.mealMoodTextSecondary) + .padding(.horizontal, 14) + .padding(.vertical, 6) + .background(Color(hex: "#F0F0F0")) + .clipShape(Capsule()) + } + .buttonStyle(.plain) + } + } + .padding(.vertical, 8) + } + + private func dayKey(for day: Int) -> String { + let keys = ["day_mon","day_tue","day_wed","day_thu","day_fri","day_sat","day_sun"] + guard day >= 0 && day < keys.count else { return "" } + return keys[day] + } + } +} + private struct ShareSheet: UIViewControllerRepresentable { let activityItems: [Any] diff --git a/MealMood/Views/Home/SocialShareSheet.swift b/MealMood/Views/Home/SocialShareSheet.swift new file mode 100644 index 0000000..49520a2 --- /dev/null +++ b/MealMood/Views/Home/SocialShareSheet.swift @@ -0,0 +1,138 @@ +import SwiftUI +import UIKit + +struct SocialShareSheet: View { + let imageURL: URL + @State private var showSystemShare = false + @State private var showCopiedToast = false + @Environment(\.dismiss) private var dismiss + + private var image: UIImage? { UIImage(contentsOfFile: imageURL.path) } + + var body: some View { + NavigationStack { + VStack(spacing: 24) { + if let img = image { + Image(uiImage: img) + .resizable() + .scaledToFit() + .frame(maxHeight: 220) + .clipShape(RoundedRectangle(cornerRadius: 14)) + .shadow(color: .black.opacity(0.12), radius: 14, x: 0, y: 6) + } + + VStack(spacing: 10) { + if canOpenInstagram { + shareOptionRow( + title: "Instagram Stories", + subtitle: String(localized: "share_instagram_subtitle"), + icon: "camera.fill", + color: Color(red: 0.80, green: 0.18, blue: 0.75), + action: shareToInstagramStories + ) + } + + shareOptionRow( + title: String(localized: "share_copy_image"), + subtitle: String(localized: "share_copy_subtitle"), + icon: "doc.on.doc.fill", + color: .mealMoodCoral, + action: copyImage + ) + + shareOptionRow( + title: String(localized: "share_more_options"), + subtitle: String(localized: "share_more_subtitle"), + icon: "square.and.arrow.up.fill", + color: Color(hex: "#4A6CF7"), + action: { showSystemShare = true } + ) + } + + Spacer() + } + .padding(20) + .navigationTitle(String(localized: "share_export_sheet_title")) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button(String(localized: "dish_cancel")) { dismiss() } + } + } + } + .toast(isShowing: $showCopiedToast, message: String(localized: "share_copied")) + .sheet(isPresented: $showSystemShare) { + SystemShareSheet(activityItems: [imageURL]) + } + } + + // MARK: - Row view + + private func shareOptionRow(title: String, subtitle: String, icon: String, color: Color, action: @escaping () -> Void) -> some View { + Button(action: action) { + HStack(spacing: 14) { + Image(systemName: icon) + .font(.system(size: 18, weight: .semibold)) + .foregroundColor(.white) + .frame(width: 44, height: 44) + .background(color) + .clipShape(RoundedRectangle(cornerRadius: 12)) + + VStack(alignment: .leading, spacing: 2) { + Text(title) + .font(.mealMoodBodyBold) + .foregroundColor(.mealMoodTextPrimary) + Text(subtitle) + .font(.mealMoodCaption) + .foregroundColor(.mealMoodTextSecondary) + } + + Spacer() + + Image(systemName: "chevron.right") + .font(.system(size: 12, weight: .semibold)) + .foregroundColor(.mealMoodTextSecondary) + } + .padding(12) + .background(Color.mealMoodSurface) + .clipShape(RoundedRectangle(cornerRadius: 14)) + } + .buttonStyle(.plain) + } + + // MARK: - Actions + + private var canOpenInstagram: Bool { + guard let url = URL(string: "instagram-stories://share") else { return false } + return UIApplication.shared.canOpenURL(url) + } + + private func shareToInstagramStories() { + guard let img = image, + let data = img.pngData(), + let url = URL(string: "instagram-stories://share?source_application=com.alexandrevazquez.mealmood") + else { return } + UIPasteboard.general.setData(data, forPasteboardType: "com.instagram.sharedSticker.backgroundImage") + UIApplication.shared.open(url) + dismiss() + } + + private func copyImage() { + guard let img = image else { return } + UIPasteboard.general.image = img + showCopiedToast = true + HapticManager.shared.notification(type: .success) + } +} + +// MARK: - System share wrapper (private) + +private struct SystemShareSheet: UIViewControllerRepresentable { + let activityItems: [Any] + + func makeUIViewController(context: Context) -> UIActivityViewController { + UIActivityViewController(activityItems: activityItems, applicationActivities: nil) + } + + func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {} +} diff --git a/MealMood/Views/Home/WeekCalendarView.swift b/MealMood/Views/Home/WeekCalendarView.swift index d20fa8c..b40589d 100644 --- a/MealMood/Views/Home/WeekCalendarView.swift +++ b/MealMood/Views/Home/WeekCalendarView.swift @@ -218,7 +218,8 @@ struct WeekCalendarView: View { dayOfWeek: day, mealType: mealType.rawValue, dishId: preferred.dishId, - isRuleOverridden: preferred.isRuleOverridden + isRuleOverridden: preferred.isRuleOverridden, + isEatingOut: preferred.isEatingOut ) } @@ -279,6 +280,14 @@ struct WeekCalendarView: View { } : nil ) .draggable("slot:\(slot.slotId.uuidString)") + } else if let slot = slot, slot.isEatingOut { + EatingOutSlotView(mealType: mealType) + .contentShape(Rectangle()) + .onTapGesture { + guard viewModel.canEditCurrentWeek else { return } + guard let live = liveSlot(for: slot) else { return } + viewModel.unmarkEatingOut(slot: live, plan: plan, settings: settings) + } } else if let slot = slot { EmptySlotView(mealType: mealType) .contentShape(Rectangle()) @@ -309,6 +318,7 @@ struct WeekCalendarView: View { let mealType: String let dishId: UUID? let isRuleOverridden: Bool + let isEatingOut: Bool } private var dishesSnapshotKey: String { @@ -354,7 +364,8 @@ struct WeekCalendarView: View { dayOfWeek: preferred.dayOfWeek, mealType: preferred.mealType, dishId: preferred.dishId, - isRuleOverridden: preferred.isRuleOverridden + isRuleOverridden: preferred.isRuleOverridden, + isEatingOut: preferred.isEatingOut ) } diff --git a/MealMood/Views/Home/WeekPlanShareView.swift b/MealMood/Views/Home/WeekPlanShareView.swift index bdf4063..f6d3e23 100644 --- a/MealMood/Views/Home/WeekPlanShareView.swift +++ b/MealMood/Views/Home/WeekPlanShareView.swift @@ -151,17 +151,29 @@ struct WeekPlanShareView: View { private var verticalBackground: some View { ZStack { - Color(hex: "#FFFDF8") + LinearGradient( + colors: [Color(hex: "#FFF7F4"), Color(hex: "#FFF3F7"), Color(hex: "#F6FFF8")], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) - Ellipse() - .fill(Color.mealMoodMint.opacity(0.28)) - .frame(width: 1100, height: 900) - .offset(x: 800, y: -1200) + Circle() + .fill(Color(hex: "#FFD0DF").opacity(0.6)) + .frame(width: 1100, height: 1100) + .blur(radius: 90) + .offset(x: 950, y: -1100) - Ellipse() - .fill(Color.mealMoodCoral.opacity(0.2)) - .frame(width: 900, height: 680) - .offset(x: -800, y: 1200) + Circle() + .fill(Color(hex: "#C0EAD8").opacity(0.55)) + .frame(width: 1000, height: 1000) + .blur(radius: 80) + .offset(x: -900, y: 1300) + + Circle() + .fill(Color(hex: "#FFE0C0").opacity(0.4)) + .frame(width: 700, height: 700) + .blur(radius: 100) + .offset(x: 100, y: 200) } } @@ -336,58 +348,91 @@ struct WeekPlanShareView: View { private var verticalLayout: some View { ZStack { - RoundedRectangle(cornerRadius: 30) - .fill(Color.white.opacity(0.95)) + RoundedRectangle(cornerRadius: 32) + .fill(Color.white.opacity(0.88)) .overlay( - RoundedRectangle(cornerRadius: 30) - .stroke(Color.mealMoodMint.opacity(0.45), lineWidth: 2) + RoundedRectangle(cornerRadius: 32) + .stroke( + LinearGradient( + colors: [Color(hex: "#F0C0CC").opacity(0.7), Color(hex: "#B8E8D4").opacity(0.7)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ), + lineWidth: 3 + ) ) - VStack(spacing: 16) { + VStack(spacing: 20) { ForEach(Array(dayRange), id: \.self) { day in - VStack(alignment: .leading, spacing: 10) { - Text(dayHeaderTitle(for: day).uppercased(with: locale)) - .font(.custom("Didot", size: 50)) - .foregroundColor(.mealMoodTextPrimary) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal, 20) - .padding(.vertical, 10) - .background(dayHeaderBackground(day: day)) - .clipShape(RoundedRectangle(cornerRadius: 12)) + HStack(spacing: 0) { + VStack(spacing: 12) { + Text(dayLongTitle(for: day).uppercased(with: locale)) + .font(.custom("Didot", size: 68)) + .foregroundColor(Color(hex: "#2C3E35")) + .multilineTextAlignment(.center) + .lineLimit(1) + .minimumScaleFactor(0.38) + .frame(maxWidth: .infinity) - VStack(alignment: .leading, spacing: 8) { + Text(dayNumberOnly(for: day)) + .font(.system(size: 96, weight: .bold, design: .rounded)) + .foregroundColor(Color(hex: "#2C3E35").opacity(0.6)) + } + .frame(width: 420) + .frame(maxHeight: .infinity) + .padding(.vertical, 28) + .background(dayHeaderBackground(day: day)) + + VStack(alignment: .leading, spacing: 24) { ForEach(mealTypes, id: \.self) { mealType in - HStack(alignment: .top, spacing: 10) { - Text("\(mealTypeLabel(mealType)):") - .font(.system(size: 34, weight: .bold, design: .serif)) - .foregroundColor(Color(hex: "#385549")) - + VStack(alignment: .leading, spacing: 8) { + HStack(spacing: 14) { + Image(systemName: mealType.icon) + .font(.system(size: 42, weight: .medium)) + .foregroundColor(mealType == .lunch ? Color(hex: "#C85E2A") : Color(hex: "#27604A")) + .frame(width: 56) + Text(mealTypeLabel(mealType).uppercased(with: locale)) + .font(.system(size: 42, weight: .bold, design: .serif)) + .foregroundColor(mealType == .lunch ? Color(hex: "#C85E2A") : Color(hex: "#27604A")) + } Text(dishName(day: day, mealType: mealType)) - .font(.system(size: 35, weight: .regular, design: .serif)) + .font(.system(size: 62, weight: .medium, design: .serif)) .foregroundColor(.mealMoodTextPrimary) .lineLimit(2) - .minimumScaleFactor(0.7) + .minimumScaleFactor(0.5) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.leading, 70) } } } - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal, 6) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) + .padding(.horizontal, 40) + .padding(.vertical, 28) + .background(Color(hex: "#FFFCF8")) } - .padding(.horizontal, 16) - .padding(.vertical, 14) - .background(Color.white) + .frame(maxHeight: .infinity) + .clipShape(RoundedRectangle(cornerRadius: 20)) .overlay( - RoundedRectangle(cornerRadius: 16) - .stroke(Color(hex: "#EEE4DE"), lineWidth: 1) + RoundedRectangle(cornerRadius: 20) + .stroke(Color(hex: "#EAD8D0"), lineWidth: 1.5) ) - .clipShape(RoundedRectangle(cornerRadius: 16)) + .shadow(color: Color.black.opacity(0.04), radius: 10, x: 0, y: 4) } } - .padding(.horizontal, 22) - .padding(.vertical, 20) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .padding(.horizontal, 28) + .padding(.vertical, 26) } } + private func dayNumberOnly(for day: Int) -> String { + let dayDate = plan.weekStartDate.addingDays(day) + let formatter = DateFormatter() + formatter.locale = locale + formatter.dateFormat = "d" + return formatter.string(from: dayDate) + } + private var footerBlock: some View { HStack(alignment: .center, spacing: 20) { VStack(alignment: .leading, spacing: 6) { diff --git a/MealMood/Views/Onboarding/WelcomeStepView.swift b/MealMood/Views/Onboarding/WelcomeStepView.swift index 989405a..23156ed 100644 --- a/MealMood/Views/Onboarding/WelcomeStepView.swift +++ b/MealMood/Views/Onboarding/WelcomeStepView.swift @@ -4,24 +4,26 @@ struct WelcomeStepView: View { var onNext: () -> Void var body: some View { - VStack(spacing: 32) { + VStack(spacing: 0) { Spacer() - AppIconPlaceholder(size: 120) + AppIconPlaceholder(size: 96) + .padding(.bottom, 12) - VStack(spacing: 12) { - Text("onboarding_welcome_subtitle") - .font(.mealMoodBody) - .foregroundColor(.mealMoodTextSecondary) - .multilineTextAlignment(.center) - } - .padding(.horizontal, 24) + Text("onboarding_welcome_subtitle") + .font(.mealMoodBody) + .foregroundColor(.mealMoodTextSecondary) + .multilineTextAlignment(.center) + .padding(.horizontal, 32) + .padding(.bottom, 24) - VStack(alignment: .leading, spacing: 16) { - BenefitRow(icon: "1.circle.fill", text: String(localized: "onboarding_welcome_step_1")) - BenefitRow(icon: "2.circle.fill", text: String(localized: "onboarding_welcome_step_2")) - BenefitRow(icon: "3.circle.fill", text: String(localized: "onboarding_welcome_step_3")) - BenefitRow(icon: "4.circle.fill", text: String(localized: "onboarding_welcome_step_4")) + weekPreview + .padding(.bottom, 24) + + VStack(alignment: .leading, spacing: 14) { + BenefitRow(icon: "wand.and.stars", text: String(localized: "onboarding_welcome_step_1")) + BenefitRow(icon: "square.and.arrow.up", text: String(localized: "onboarding_welcome_step_3")) + BenefitRow(icon: "icloud", text: String(localized: "onboarding_welcome_step_4")) } .padding(.horizontal, 40) @@ -33,6 +35,115 @@ struct WelcomeStepView: View { } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) } + + // MARK: - Week preview + + private var weekPreview: some View { + VStack(alignment: .leading, spacing: 8) { + Text(String(localized: "onboarding_welcome_preview_label")) + .font(.mealMoodCaption) + .foregroundColor(.mealMoodTextSecondary) + .padding(.horizontal, 24) + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 6) { + ForEach(sampleDays, id: \.name) { day in + VStack(spacing: 4) { + Text(day.name) + .font(.system(size: 10, weight: .bold)) + .foregroundColor(.mealMoodTextPrimary) + .frame(maxWidth: .infinity) + .padding(.vertical, 5) + .background(day.color) + .clipShape(RoundedRectangle(cornerRadius: 8)) + + Text(day.dish) + .font(.system(size: 10, weight: .medium)) + .foregroundColor(.mealMoodTextPrimary) + .multilineTextAlignment(.center) + .lineLimit(2) + .minimumScaleFactor(0.8) + .frame(maxWidth: .infinity, minHeight: 38) + .padding(.horizontal, 4) + .padding(.vertical, 5) + .background(Color.white.opacity(0.92)) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color(hex: "#EEE4DE"), lineWidth: 1) + ) + } + .frame(width: 66) + } + } + .padding(.horizontal, 20) + .padding(.vertical, 8) + } + .background( + RoundedRectangle(cornerRadius: 14) + .fill(Color.mealMoodBackground) + .overlay( + RoundedRectangle(cornerRadius: 14) + .stroke(Color.mealMoodCoral.opacity(0.25), lineWidth: 1) + ) + ) + .padding(.horizontal, 16) + } + } + + private var sampleDays: [(name: String, dish: String, color: Color)] { + let lang = Locale.current.language.languageCode?.identifier ?? "en" + switch lang { + case "es": + return [ + ("Lun", "Pasta al horno", Color(hex: "#FFD5D9")), + ("Mar", "Pollo asado", Color(hex: "#FFE1C8")), + ("Mié", "Lentejas", Color(hex: "#FFF0C8")), + ("Jue", "Salmón", Color(hex: "#DFF2CC")), + ("Vie", "Tortilla", Color(hex: "#D3F0E7")) + ] + case "fr": + return [ + ("Lun", "Pâtes gratinées", Color(hex: "#FFD5D9")), + ("Mar", "Poulet rôti", Color(hex: "#FFE1C8")), + ("Mer", "Lentilles", Color(hex: "#FFF0C8")), + ("Jeu", "Saumon", Color(hex: "#DFF2CC")), + ("Ven", "Omelette", Color(hex: "#D3F0E7")) + ] + case "de": + return [ + ("Mo", "Nudeln", Color(hex: "#FFD5D9")), + ("Di", "Hähnchen", Color(hex: "#FFE1C8")), + ("Mi", "Linsensuppe", Color(hex: "#FFF0C8")), + ("Do", "Lachs", Color(hex: "#DFF2CC")), + ("Fr", "Omelett", Color(hex: "#D3F0E7")) + ] + case "it": + return [ + ("Lun", "Pasta al forno", Color(hex: "#FFD5D9")), + ("Mar", "Pollo arrosto", Color(hex: "#FFE1C8")), + ("Mer", "Lenticchie", Color(hex: "#FFF0C8")), + ("Gio", "Salmone", Color(hex: "#DFF2CC")), + ("Ven", "Frittata", Color(hex: "#D3F0E7")) + ] + case "pt": + return [ + ("Seg", "Macarrão", Color(hex: "#FFD5D9")), + ("Ter", "Frango assado", Color(hex: "#FFE1C8")), + ("Qua", "Lentilhas", Color(hex: "#FFF0C8")), + ("Qui", "Salmão", Color(hex: "#DFF2CC")), + ("Sex", "Omelete", Color(hex: "#D3F0E7")) + ] + default: + return [ + ("Mon", "Baked pasta", Color(hex: "#FFD5D9")), + ("Tue", "Roast chicken", Color(hex: "#FFE1C8")), + ("Wed", "Lentil soup", Color(hex: "#FFF0C8")), + ("Thu", "Salmon", Color(hex: "#DFF2CC")), + ("Fri", "Omelette", Color(hex: "#D3F0E7")) + ] + } + } } private struct BenefitRow: View { @@ -42,7 +153,7 @@ private struct BenefitRow: View { var body: some View { HStack(spacing: 12) { Image(systemName: icon) - .font(.system(size: 20)) + .font(.system(size: 18)) .foregroundColor(.mealMoodCoral) .frame(width: 28) diff --git a/MealMood/Views/Premium/PremiumView.swift b/MealMood/Views/Premium/PremiumView.swift index 2c73390..e2a9c7d 100644 --- a/MealMood/Views/Premium/PremiumView.swift +++ b/MealMood/Views/Premium/PremiumView.swift @@ -147,6 +147,7 @@ struct PremiumView: View { } private func purchase(_ product: Product) async { + AnalyticsService.logPremiumUpgradeTapped(source: "premium_view") let result = await storeManager.purchase(product) switch result { case .success: diff --git a/MealMood/Views/Settings/SettingsView.swift b/MealMood/Views/Settings/SettingsView.swift index 32d86f8..a93def4 100644 --- a/MealMood/Views/Settings/SettingsView.swift +++ b/MealMood/Views/Settings/SettingsView.swift @@ -238,6 +238,7 @@ struct SettingsView: View { .foregroundColor(.mealMoodTextPrimary) .tint(.mealMoodCoral) .onAppear { + AnalyticsService.logScreenView("Settings") if settings.syncEnabled { let hasCalendar = viewModel.ensureValidCalendarSelection(settings: settings) settings.syncEnabled = hasCalendar diff --git a/MealMoodTests/PremiumSyncServiceTests.swift b/MealMoodTests/PremiumSyncServiceTests.swift new file mode 100644 index 0000000..443712a --- /dev/null +++ b/MealMoodTests/PremiumSyncServiceTests.swift @@ -0,0 +1,169 @@ +import XCTest +import SwiftData +@testable import MealMood + +@MainActor +final class PremiumSyncServiceTests: XCTestCase { + + var service: PremiumSyncService! + + override func setUp() { + super.setUp() + service = PremiumSyncService() + } + + // MARK: - Active subscription + + func testSync_activeSubscription_whenFree_upgradesTo_premium() async { + service.hasActiveSubscription = { true } + let result = await service.sync(currentIsPremium: false, isColdLaunch: true) + XCTAssertTrue(result, "Active subscription should upgrade free user to premium") + } + + func testSync_activeSubscription_whenAlreadyPremium_staysPremium() async { + service.hasActiveSubscription = { true } + let result = await service.sync(currentIsPremium: true, isColdLaunch: true) + XCTAssertTrue(result, "Active subscription keeps premium status") + } + + func testSync_activeSubscription_onForeground_upgradesCorrectly() async { + service.hasActiveSubscription = { true } + let result = await service.sync(currentIsPremium: false, isColdLaunch: false) + XCTAssertTrue(result, "Active subscription upgrades on foreground activation") + } + + // MARK: - No subscription, was free + + func testSync_noSubscription_whenFree_coldLaunch_staysFree() async { + service.hasActiveSubscription = { false } + let result = await service.sync(currentIsPremium: false, isColdLaunch: true) + XCTAssertFalse(result, "No subscription and was free → stays free") + } + + func testSync_noSubscription_whenFree_foreground_staysFree() async { + service.hasActiveSubscription = { false } + let result = await service.sync(currentIsPremium: false, isColdLaunch: false) + XCTAssertFalse(result, "No subscription and was free → stays free on foreground") + } + + // MARK: - No subscription, was premium (expired or timing issue) + + func testSync_noSubscription_whenPremium_coldLaunch_downgrades() async { + // Subscription expired between sessions → downgrade on cold launch + service.hasActiveSubscription = { false } + let result = await service.sync(currentIsPremium: true, isColdLaunch: true) + XCTAssertFalse(result, "Expired subscription should downgrade on cold launch") + } + + func testSync_noSubscription_whenPremium_foreground_keepsPremium() async { + // CRITICAL: StoreKit's Transaction.currentEntitlements can return empty for a + // few seconds immediately after a purchase completes. We must NOT downgrade + // mid-session — let the Transaction.updates listener handle real revocations. + service.hasActiveSubscription = { false } + let result = await service.sync(currentIsPremium: true, isColdLaunch: false) + XCTAssertTrue(result, "Should not downgrade premium on foreground activation (StoreKit timing protection)") + } + + // MARK: - Purchase flow simulation + + func testPurchaseFlow_storeKitReturnsFalseRightAfterPurchase_premiumPreserved() async { + // Simulate the exact Apple Review rejection scenario: + // 1. User purchases successfully (isPremium = true is set by PremiumView) + // 2. StoreKit payment sheet closes → scene becomes .active + // 3. syncPremiumStatus(isColdLaunch: false) runs + // 4. StoreKit returns false momentarily (receipt not yet propagated) + // 5. Expected: premium status is PRESERVED + service.hasActiveSubscription = { false } + let result = await service.sync(currentIsPremium: true, isColdLaunch: false) + XCTAssertTrue(result, "Premium must be preserved immediately after purchase even if StoreKit returns false") + } + + func testPurchaseFlow_storeKitReturnsTrueAfterPurchase_premiumConfirmed() async { + // Normal happy path: purchase completes, StoreKit confirms + service.hasActiveSubscription = { true } + let result = await service.sync(currentIsPremium: false, isColdLaunch: false) + XCTAssertTrue(result, "Purchase confirmed by StoreKit → premium activated") + } + + // MARK: - Multiple sync cycles + + func testMultipleForegroundSyncs_withNoSubscription_afterPurchase_keepsPremium() async { + // User purchases → goes to background 5 times → StoreKit never confirms + // (extreme timing issue). Premium should be preserved in all foreground cycles. + service.hasActiveSubscription = { false } + var currentPremium = true // Set by purchase + + for _ in 1...5 { + currentPremium = await service.sync(currentIsPremium: currentPremium, isColdLaunch: false) + } + XCTAssertTrue(currentPremium, "Premium must survive multiple foreground cycles when StoreKit timing fails") + } + + func testColdLaunchAfterExpiry_downgrades() async { + // User had premium, subscription expired, reopens app next day + service.hasActiveSubscription = { false } + let result = await service.sync(currentIsPremium: true, isColdLaunch: true) + XCTAssertFalse(result, "Expired subscription must be detected and downgraded on next cold launch") + } +} + +// MARK: - iCloud sync isolation tests + +@MainActor +final class ICloudSyncPremiumIsolationTests: XCTestCase { + + func testICloudSync_doesNotOverwritePremiumStatus() async throws { + // Arrange: in-memory SwiftData container + let config = ModelConfiguration(isStoredInMemoryOnly: true) + let container = try ModelContainer( + for: AppSettings.self, Tag.self, Dish.self, WeekPlan.self, MealSlot.self, + configurations: config + ) + let context = container.mainContext + + let settings = AppSettings() + settings.isPremium = true + context.insert(settings) + try context.save() + + // Verify initial state + let before = try context.fetch(FetchDescriptor()).first + XCTAssertTrue(before?.isPremium == true, "Setup: isPremium should be true") + + // Act: simulate what ICloudSyncService.apply does — it should NOT touch isPremium + // The fix removes isPremium from SettingsPayload, so apply() can never set it. + // We verify by checking that isPremium is unchanged after any settings update. + let settingsAfter = try context.fetch(FetchDescriptor()).first + // Simulate a settings-only update (language change etc.) without touching isPremium + settingsAfter?.language = "es" + try context.save() + + // Assert: isPremium untouched + let final = try context.fetch(FetchDescriptor()).first + XCTAssertTrue(final?.isPremium == true, "isPremium must not be overwritten by settings sync operations") + XCTAssertEqual(final?.language, "es", "Other settings should still be updated") + } + + func testPremiumStatus_persistsAcrossSettingsChanges() async throws { + let config = ModelConfiguration(isStoredInMemoryOnly: true) + let container = try ModelContainer( + for: AppSettings.self, Tag.self, Dish.self, WeekPlan.self, MealSlot.self, + configurations: config + ) + let context = container.mainContext + + let settings = AppSettings() + settings.isPremium = true + context.insert(settings) + try context.save() + + // Simulate multiple setting updates + let s = try context.fetch(FetchDescriptor()).first! + s.includeWeekends = true + s.mealWindows = "dinnerOnly" + try context.save() + + let result = try context.fetch(FetchDescriptor()).first + XCTAssertTrue(result?.isPremium == true, "isPremium must persist through unrelated settings changes") + } +} diff --git a/MealMoodWidget/Info.plist b/MealMoodWidget/Info.plist new file mode 100644 index 0000000..7bef3c8 --- /dev/null +++ b/MealMoodWidget/Info.plist @@ -0,0 +1,27 @@ + + + + + CFBundleDisplayName + MealMood + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 1.0.3 + CFBundleVersion + 21 + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + + diff --git a/MealMoodWidget/MealMoodWidget.entitlements b/MealMoodWidget/MealMoodWidget.entitlements new file mode 100644 index 0000000..be6cdf6 --- /dev/null +++ b/MealMoodWidget/MealMoodWidget.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.alexandrevazquez.mealmood + + + diff --git a/MealMoodWidget/MealMoodWidget.swift b/MealMoodWidget/MealMoodWidget.swift new file mode 100644 index 0000000..bd813f3 --- /dev/null +++ b/MealMoodWidget/MealMoodWidget.swift @@ -0,0 +1,233 @@ +import WidgetKit +import SwiftUI +import Foundation + +// MARK: - Shared data model (mirrors WidgetDataStore in main app) + +private struct TodayMealData: Codable { + let lunch: String? + let dinner: String? + let lunchLabel: String + let dinnerLabel: String + let weekdayName: String + let updatedAt: Date +} + +private enum WidgetStore { + static let appGroupID = "group.com.alexandrevazquez.mealmood" + static let key = "mealmood.today_meals" + + static func read() -> TodayMealData? { + guard let defaults = UserDefaults(suiteName: appGroupID), + let data = defaults.data(forKey: key) else { return nil } + return try? JSONDecoder().decode(TodayMealData.self, from: data) + } +} + +// MARK: - Timeline + +struct TodayMealEntry: TimelineEntry { + let date: Date + let lunch: String? + let dinner: String? + let lunchLabel: String + let dinnerLabel: String + let weekdayName: String +} + +struct Provider: TimelineProvider { + func placeholder(in context: Context) -> TodayMealEntry { + TodayMealEntry(date: Date(), lunch: "Pasta al pesto", dinner: "Salmón al horno", + lunchLabel: "Comida", dinnerLabel: "Cena", weekdayName: "Lunes") + } + + func getSnapshot(in context: Context, completion: @escaping (TodayMealEntry) -> Void) { + let data = WidgetStore.read() + completion(entry(from: data)) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + let data = WidgetStore.read() + let nextMidnight = Calendar.current.startOfDay(for: Calendar.current.date(byAdding: .day, value: 1, to: Date())!) + completion(Timeline(entries: [entry(from: data)], policy: .after(nextMidnight))) + } + + private func entry(from data: TodayMealData?) -> TodayMealEntry { + TodayMealEntry( + date: Date(), + lunch: data?.lunch, + dinner: data?.dinner, + lunchLabel: data?.lunchLabel ?? "Lunch", + dinnerLabel: data?.dinnerLabel ?? "Dinner", + weekdayName: data?.weekdayName ?? "" + ) + } +} + +// MARK: - Colors + +private extension Color { + static let mmCoral = Color(red: 0.93, green: 0.42, blue: 0.31) + static let mmBgWarm = Color(red: 1.00, green: 0.94, blue: 0.91) + static let mmBgMint = Color(red: 0.93, green: 0.98, blue: 0.95) + static let mmText = Color(red: 0.13, green: 0.13, blue: 0.13) + static let mmSubtext = Color(red: 0.55, green: 0.55, blue: 0.55) + static let mmLunchOrange = Color(red: 0.90, green: 0.55, blue: 0.18) + static let mmDinnerBlue = Color(red: 0.36, green: 0.48, blue: 0.78) +} + +// MARK: - Small widget + +private struct SmallWidgetView: View { + let entry: TodayMealEntry + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + HStack(spacing: 4) { + Image(systemName: "fork.knife") + .font(.system(size: 10, weight: .bold)) + .foregroundColor(.mmCoral) + Text("MealMood") + .font(.system(size: 11, weight: .bold)) + .foregroundColor(.mmCoral) + Spacer() + } + + Text(entry.weekdayName) + .font(.system(size: 10, weight: .medium)) + .foregroundColor(.mmSubtext) + .lineLimit(1) + .padding(.top, 2) + .padding(.bottom, 10) + + Spacer() + + mealRow(icon: "sun.max.fill", label: entry.lunchLabel, + name: entry.lunch, color: .mmLunchOrange) + .padding(.bottom, 8) + + mealRow(icon: "moon.stars.fill", label: entry.dinnerLabel, + name: entry.dinner, color: .mmDinnerBlue) + } + .padding(12) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) + .background( + LinearGradient(colors: [.mmBgWarm, .mmBgMint], + startPoint: .topLeading, endPoint: .bottomTrailing) + ) + } + + private func mealRow(icon: String, label: String, name: String?, color: Color) -> some View { + HStack(spacing: 8) { + Image(systemName: icon) + .font(.system(size: 13)) + .foregroundColor(color) + .frame(width: 18) + VStack(alignment: .leading, spacing: 1) { + Text(label.uppercased()) + .font(.system(size: 8, weight: .semibold)) + .foregroundColor(.mmSubtext) + Text(name ?? "—") + .font(.system(size: 13, weight: .semibold)) + .foregroundColor(.mmText) + .lineLimit(2) + .minimumScaleFactor(0.75) + } + } + } +} + +// MARK: - Medium widget + +private struct MediumWidgetView: View { + let entry: TodayMealEntry + + var body: some View { + HStack(spacing: 0) { + VStack(alignment: .leading, spacing: 6) { + Image(systemName: "fork.knife") + .font(.system(size: 18, weight: .bold)) + .foregroundColor(.mmCoral) + Spacer() + Text(entry.weekdayName) + .font(.system(size: 16, weight: .bold)) + .foregroundColor(.mmText) + .lineLimit(1) + .minimumScaleFactor(0.7) + Text(Date(), style: .date) + .font(.system(size: 11, weight: .medium)) + .foregroundColor(.mmSubtext) + } + .padding(14) + .frame(width: 112) + .frame(maxHeight: .infinity, alignment: .leading) + .background(Color.mmBgWarm) + + Rectangle() + .fill(Color.mmCoral.opacity(0.2)) + .frame(width: 1) + + VStack(alignment: .leading, spacing: 14) { + mediumRow(icon: "sun.max.fill", label: entry.lunchLabel, + name: entry.lunch, color: .mmLunchOrange) + Divider() + mediumRow(icon: "moon.stars.fill", label: entry.dinnerLabel, + name: entry.dinner, color: .mmDinnerBlue) + } + .padding(14) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) + .background(Color.white) + } + } + + private func mediumRow(icon: String, label: String, name: String?, color: Color) -> some View { + HStack(spacing: 10) { + Image(systemName: icon) + .font(.system(size: 16)) + .foregroundColor(color) + .frame(width: 22) + VStack(alignment: .leading, spacing: 2) { + Text(label.uppercased()) + .font(.system(size: 9, weight: .semibold)) + .foregroundColor(.mmSubtext) + Text(name ?? "—") + .font(.system(size: 15, weight: .semibold)) + .foregroundColor(.mmText) + .lineLimit(2) + } + } + } +} + +// MARK: - Widget entry view + +struct MealMoodWidgetView: View { + var entry: Provider.Entry + @Environment(\.widgetFamily) var family + + var body: some View { + switch family { + case .systemMedium: + MediumWidgetView(entry: entry) + default: + SmallWidgetView(entry: entry) + } + } +} + +// MARK: - Widget definition + +@main +struct MealMoodWidget: Widget { + let kind = "MealMoodWidget" + + var body: some WidgetConfiguration { + StaticConfiguration(kind: kind, provider: Provider()) { entry in + MealMoodWidgetView(entry: entry) + .containerBackground(for: .widget) { Color.white } + } + .configurationDisplayName("MealMood") + .description("Today's lunch and dinner at a glance.") + .supportedFamilies([.systemSmall, .systemMedium]) + } +} diff --git a/fastlane/Appfile b/fastlane/Appfile new file mode 100644 index 0000000..0d73f39 --- /dev/null +++ b/fastlane/Appfile @@ -0,0 +1,8 @@ +app_identifier("com.alexandrevazquez.mealmood") # The bundle identifier of your app +apple_id("alexandre.vazquez@gmail.com") # Your Apple Developer Portal username + +itc_team_id("128443966") # App Store Connect Team ID +team_id("2825Q76T7H") # Developer Portal Team ID + +# For more information about the Appfile, see: +# https://docs.fastlane.tools/advanced/#appfile diff --git a/fastlane/Deliverfile b/fastlane/Deliverfile new file mode 100644 index 0000000..1a8c05a --- /dev/null +++ b/fastlane/Deliverfile @@ -0,0 +1 @@ +run_precheck_before_submit(false) diff --git a/fastlane/Preview.html b/fastlane/Preview.html new file mode 100644 index 0000000..4dcf528 --- /dev/null +++ b/fastlane/Preview.html @@ -0,0 +1,784 @@ + + + + deliver - MealMood: Family Meal Planner + + + + + + +
+ + + +
+ + +
+ de-DE: MealMood: Wochenmenü-Planer +
+ + + +
+ Subtitle: Planen, teilen und ausdrucken +
+ + +
+ + + + +
+ support_url: https://mealmood.app + +
+ + + + + +
+ + +
+
Keywords
+
    + +
  • Essensplaner
  • + +
  • Wochenplan
  • + +
  • Familienessen
  • + +
  • Menüplan
  • + +
  • Mittagessen planen
  • + +
  • Abendessen
  • + +
  • Rezepte
  • + +
  • Speiseplan
  • + +
+
+ + + +
+
Description
+
+ Plane die Mahlzeiten deiner Familie in Minuten — nicht in Stunden.

MealMood ist der einfache, schöne Wochenmenü-Planer für vielbeschäftigte Familien. Füge deine Lieblingsgerichte hinzu, stelle deine Vorlieben ein und lass die App den Rest erledigen.

IN SEKUNDEN PLANEN
Erstelle eine vollständige Woche mit Mittag- und Abendessen auf Knopfdruck. Die intelligente Autovervollständigung füllt deinen Planer mit deiner Gerichtebibliothek und Tag-Regeln — ohne Wiederholungen, ohne Stress.

DEINE GERICHTEBIBLIOTHEK
Füge alle Haushaltsfavoriten hinzu und beschrifte sie mit smarten Tags. Stelle Regeln wie „maximal 2 Mal pro Woche" oder „nur Abendessen" ein — MealMood hält sie automatisch ein.

WUNDERSCHÖNE EXPORTS — KÜHLSCHRANKFERTIG
Teile deinen Wochenplan als Bild in 3 Stilen:
• Klassisches Raster
• Eleganter Stundenplan-Stil
• Hochformat A4 — ausdrucken und an den Kühlschrank hängen

IMMER SYNCHRONISIERT
iCloud hält deinen Plan auf allen Apple-Geräten aktuell. Ein Plan, überall sichtbar.

KALENDERINTEGRATION
Synchronisiere Mahlzeiten direkt mit dem Apple-Kalender, damit die ganze Familie immer weiß, was es zu essen gibt.

— — —

KOSTENLOS
• Bis zu 20 Gerichte
• Planung der aktuellen Woche
• Mit Werbung

PREMIUM (Monatsabo)
• Unbegrenzte Gerichte
• Jede zukünftige Woche planen
• Erweiterte Tag-Regeln
• Wöchentliche Menü-Exports
• Werbefrei

— — —

Verfügbar auf Deutsch, Spanisch, Englisch, Französisch, Italienisch und Portugiesisch. +
+
+ + + +
+
Changelog
+ Intelligentere Planung in diesem Update:

• Die Auto-Vervollständigung vermeidet jetzt die Wiederholung desselben Gerichts, wenn Alternativen verfügbar sind — abwechslungsreichere und ausgewogenere Wochen.
• Mehrere Gerichte hinzufügen geht jetzt schneller: Das Formular bleibt nach dem Speichern offen, damit du mehrere hintereinander hinzufügen kannst.
• Kalendersynchronisation korrigiert: Ereignisse werden jetzt aktualisiert statt dupliziert, wenn erneut synchronisiert wird.
• Einrichtung vereinfacht: Gerichte sind beim ersten Setup jetzt optional — du kannst sie später hinzufügen, wenn du möchtest.

Allgemeine Verbesserungen und Fehlerbehebungen.
+
+ + + +
+
Promotional Text
+ Plane Mittag- und Abendessen für die ganze Woche in Sekunden. 3 Export-Stile — inkl. A4 für den Kühlschrank. Kostenlos starten. +
+ + +
+
Screenshots
+ + + + + + + +
+ +
+ + +
+ en-US: MealMood: Family Meal Planner + +
+ + + +
+ Subtitle: Plan Your Meals, Share & Print +
+ + +
+ + + + +
+ support_url: https://mealmood.app + +
+ + + + +
+ marketing_url: https://mealmood.app + +
+ + +
+ + +
+
Keywords
+
    + +
  • meal planner
  • + +
  • weekly planner
  • + +
  • dinner ideas
  • + +
  • family meals
  • + +
  • lunch planner
  • + +
  • meal schedule
  • + +
  • weekly menu
  • + +
  • recipe
  • + +
+
+ + + +
+
Description
+
+ Plan your family's weekly meals in minutes — not hours.

MealMood is the simple, beautiful weekly meal planner for busy families. Build your dish library, plan lunches and dinners for the week, and share a gorgeous printable menu — straight to the fridge.

PLAN IN SECONDS
Create a full week of lunches and dinners with one tap. MealMood's smart auto-complete fills your planner using your dish library and tag rules — no repetition, no stress.

YOUR DISH LIBRARY
Add all your household favorites and label them with smart tags. Set rules like "max 2 times per week" or "dinner only" and MealMood will respect them automatically when auto-completing your week.

BEAUTIFUL EXPORTS — FRIDGE-READY
Share your weekly menu as a gorgeous image in 3 styles:
• Classic grid layout
• Elegant school timetable style
• Vertical A4 portrait — print it and stick it on the fridge

ALWAYS IN SYNC
iCloud sync keeps your plan up to date across all your Apple devices. One plan, visible everywhere.

CALENDAR INTEGRATION
Sync your meals directly to Apple Calendar so the whole family always knows what's for lunch and dinner.

— — —

FREE PLAN
• Up to 20 dishes
• Plan the current week
• Ad-supported

PREMIUM (monthly subscription)
• Unlimited dishes
• Plan any future week
• Advanced tag rules
• Beautiful week exports
• Ad-free experience

— — —

Available in English, Spanish, French, German, Italian and Portuguese. +
+
+ + + +
+
Changelog
+ Smarter meal planning in this update:

• Auto-assign now avoids repeating the same dish twice when alternatives are available — smarter, more varied weekly plans.
• Adding multiple dishes is now faster: the form stays open after saving so you can add several in a row.
• Calendar sync fixed: events are now updated instead of duplicated when re-syncing.
• Onboarding simplified: dishes are now optional during setup — jump in and add them later if you prefer.

Various improvements and bug fixes.
+
+ + + +
+
Promotional Text
+ Plan lunches & dinners for the whole week in seconds. 3 beautiful export styles — including A4 for the fridge. Free to start, Premium unlocks everything. +
+ + +
+
Screenshots
+ + + + + + + + +

iPad Pro (12.9-inch) (3rd generation)

+
+ + + + + + + + + + + + +
+ + + +

iPhone 14 Pro Max

+
+ + + + + + + + + + + + +
+ + +
+ +
+ + +
+ es-ES: MealMood: Planificador Semanal +
+ + + +
+ Subtitle: Planifica, comparte e imprime +
+ + +
+ + + + +
+ support_url: https://mealmood.app + +
+ + + + + +
+ + +
+
Keywords
+
    + +
  • planificador comidas
  • + +
  • menú semanal
  • + +
  • comidas familia
  • + +
  • qué comer
  • + +
  • plan semanal
  • + +
  • cenas semana
  • + +
  • ideas cenas
  • + +
+
+ + + +
+
Description
+
+ Planifica las comidas de la semana en minutos, no en horas.

MealMood es el planificador de menú semanal simple y bonito para familias ocupadas. Añade tus platos favoritos, configura tus preferencias y deja que la app haga el resto.

PLANIFICA EN SEGUNDOS
Crea una semana completa de comidas y cenas con un toque. El autocompletado inteligente rellena tu planificador usando tu biblioteca de platos y reglas de etiquetas — sin repeticiones, sin estrés.

TU BIBLIOTECA DE PLATOS
Añade todos los favoritos de casa y etiquétalos con reglas inteligentes. Configura reglas como "máximo 2 veces por semana" o "solo cenas" y MealMood las respetará automáticamente.

EXPORTACIONES PRECIOSAS — LISTAS PARA LA NEVERA
Comparte tu menú semanal como imagen en 3 estilos:
• Cuadrícula clásica
• Horario escolar elegante
• Retrato A4 vertical — imprímelo y ponlo en la nevera

SIEMPRE SINCRONIZADO
iCloud mantiene tu plan actualizado en todos tus dispositivos Apple. Un plan, visible en todas partes.

INTEGRACIÓN CON CALENDARIO
Sincroniza tus comidas directamente con el Calendario de Apple para que toda la familia sepa siempre qué hay de comer.

— — —

PLAN GRATUITO
• Hasta 20 platos
• Planificación de la semana actual
• Con anuncios

PREMIUM (suscripción mensual)
• Platos ilimitados
• Planifica cualquier semana futura
• Reglas de etiquetas avanzadas
• Exportaciones del menú semanal
• Sin anuncios

— — —

Disponible en español, inglés, francés, alemán, italiano y portugués. +
+
+ + + +
+
Changelog
+ Planificación más inteligente en esta actualización:

• El auto-completar ahora evita repetir el mismo plato dos veces si hay alternativas — semanas más variadas y equilibradas.
• Añadir varios platos es más rápido: el formulario se queda abierto tras guardar para poder añadir varios seguidos.
• Sincronización con el calendario corregida: los eventos ahora se actualizan en lugar de duplicarse al volver a sincronizar.
• Incorporación simplificada: los platos ahora son opcionales durante la configuración inicial — puedes añadirlos después si lo prefieres.

Mejoras generales y corrección de errores.
+
+ + + +
+
Promotional Text
+ Planifica comidas y cenas de toda la semana en segundos. 3 estilos de exportación — incluido A4 para la nevera. Gratis para empezar. +
+ + +
+
Screenshots
+ + + + + + + +
+ +
+ + +
+ fr-FR: MealMood: Planificateur Repas +
+ + + +
+ Subtitle: Planifiez, partagez, imprimez +
+ + +
+ + + + +
+ support_url: https://mealmood.app + +
+ + + + + +
+ + +
+
Keywords
+
    + +
  • planificateur repas
  • + +
  • menu semaine
  • + +
  • repas famille
  • + +
  • idées dîner
  • + +
  • planning repas
  • + +
  • menu hebdomadaire
  • + +
  • recettes
  • + +
+
+ + + +
+
Description
+
+ Planifiez les repas de la semaine en quelques minutes, pas en heures.

MealMood est le planificateur de menus hebdomadaire simple et beau pour les familles actives. Ajoutez vos plats préférés, définissez vos préférences et laissez l'app faire le reste.

PLANIFIEZ EN QUELQUES SECONDES
Créez une semaine complète de déjeuners et dîners en un tap. La saisie automatique intelligente remplit votre planning avec votre bibliothèque de plats et vos règles d'étiquettes — sans répétition, sans stress.

VOTRE BIBLIOTHÈQUE DE PLATS
Ajoutez tous vos favoris et étiquetez-les avec des règles intelligentes. Définissez des règles comme « maximum 2 fois par semaine » ou « dîner uniquement » et MealMood les respectera automatiquement.

EXPORTS MAGNIFIQUES — PRÊTS POUR LE FRIGO
Partagez votre menu en image dans 3 styles :
• Grille classique
• Emploi du temps élégant
• Portrait A4 vertical — imprimez-le et collez-le sur le frigo

TOUJOURS SYNCHRONISÉ
iCloud maintient votre planning à jour sur tous vos appareils Apple. Un plan, visible partout.

INTÉGRATION CALENDRIER
Synchronisez vos repas directement dans Calendrier Apple pour que toute la famille sache toujours ce qu'il y a à manger.

— — —

GRATUIT
• Jusqu'à 20 plats
• Planification de la semaine en cours
• Avec publicités

PREMIUM (abonnement mensuel)
• Plats illimités
• Planifiez n'importe quelle semaine future
• Règles d'étiquettes avancées
• Exports du menu hebdomadaire
• Sans publicité

— — —

Disponible en français, espagnol, anglais, allemand, italien et portugais. +
+
+ + + +
+
Changelog
+ Une planification plus intelligente dans cette mise à jour :

• L'auto-complétion évite désormais de répéter le même plat deux fois lorsqu'il y a des alternatives — des semaines plus variées et équilibrées.
• Ajouter plusieurs plats est plus rapide : le formulaire reste ouvert après l'enregistrement pour en ajouter plusieurs à la suite.
• Synchronisation du calendrier corrigée : les événements sont maintenant mis à jour au lieu d'être dupliqués lors d'une nouvelle synchronisation.
• Démarrage simplifié : les plats sont désormais optionnels pendant la configuration initiale — vous pouvez les ajouter plus tard si vous préférez.

Améliorations générales et corrections de bugs.
+
+ + + +
+
Promotional Text
+ Planifiez déjeuners et dîners pour toute la semaine en secondes. 3 styles d'export — dont le A4 pour le frigo. Gratuit pour commencer. +
+ + +
+
Screenshots
+ + + + + + + +
+ +
+ + +
+ it: MealMood: Pianificatore Pasti +
+ + + +
+ Subtitle: Pianifica, condividi, stampa +
+ + +
+ + + + +
+ support_url: https://mealmood.app + +
+ + + + + +
+ + +
+
Keywords
+
    + +
  • pianificatore pasti
  • + +
  • menu settimanale
  • + +
  • pasti famiglia
  • + +
  • idee cena
  • + +
  • piano pasti
  • + +
  • cosa mangiare
  • + +
  • ricette
  • + +
+
+ + + +
+
Description
+
+ Pianifica i pasti della famiglia in pochi minuti, non in ore.

MealMood è il pianificatore di menu settimanale semplice e bello per le famiglie indaffarate. Aggiungi i tuoi piatti preferiti, imposta le tue preferenze e lascia che l'app faccia il resto.

PIANIFICA IN SECONDI
Crea una settimana completa di pranzi e cene con un tocco. Il completamento automatico intelligente riempie il tuo planner con la libreria di piatti e le regole dei tag — niente ripetizioni, niente stress.

LA TUA LIBRERIA DI PIATTI
Aggiungi tutti i preferiti di casa e etichettali con regole intelligenti. Imposta regole come "massimo 2 volte a settimana" o "solo cena" e MealMood le rispetterà automaticamente.

EXPORT BELLISSIMI — PRONTI PER IL FRIGO
Condividi il tuo menu settimanale come immagine in 3 stili:
• Griglia classica
• Elegante orario scolastico
• Ritratto A4 verticale — stampalo e mettilo sul frigorifero

SEMPRE SINCRONIZZATO
iCloud mantiene il tuo piano aggiornato su tutti i dispositivi Apple. Un piano, visibile ovunque.

INTEGRAZIONE CALENDARIO
Sincronizza i pasti direttamente con Calendario Apple così tutta la famiglia sa sempre cosa c'è da mangiare.

— — —

GRATUITO
• Fino a 20 piatti
• Pianificazione della settimana corrente
• Con pubblicità

PREMIUM (abbonamento mensile)
• Piatti illimitati
• Pianifica qualsiasi settimana futura
• Regole tag avanzate
• Export menu settimanale
• Senza pubblicità

— — —

Disponibile in italiano, spagnolo, inglese, francese, tedesco e portoghese. +
+
+ + + +
+
Changelog
+ Pianificazione più intelligente in questo aggiornamento:

• Il completamento automatico ora evita di ripetere lo stesso piatto due volte quando ci sono alternative — settimane più varie ed equilibrate.
• Aggiungere più piatti è più veloce: il modulo rimane aperto dopo il salvataggio per poterne aggiungere più di uno di seguito.
• Sincronizzazione del calendario corretta: gli eventi vengono ora aggiornati invece di essere duplicati quando si risincronizza.
• Configurazione semplificata: i piatti sono ora opzionali durante la configurazione iniziale — puoi aggiungerli dopo se preferisci.

Miglioramenti generali e correzioni di bug.
+
+ + + +
+
Promotional Text
+ Pianifica pranzi e cene per tutta la settimana in secondi. 3 stili di export — incluso A4 per il frigo. Gratis per iniziare. +
+ + +
+
Screenshots
+ + + + + + + +
+ +
+ + +
+ pt-BR: MealMood: Planejador Semanal +
+ + + +
+ Subtitle: Planeje, compartilhe e imprima +
+ + +
+ + + + +
+ support_url: https://mealmood.app + +
+ + + + + +
+ + +
+
Keywords
+
    + +
  • planejador refeições
  • + +
  • cardápio semanal
  • + +
  • refeições família
  • + +
  • o que comer
  • + +
  • plano alimentar
  • + +
  • jantar
  • + +
  • receitas
  • + +
+
+ + + +
+
Description
+
+ Planeje as refeições da família em minutos — não em horas.

MealMood é o planejador de cardápio semanal simples e bonito para famílias ocupadas. Adicione seus pratos favoritos, configure suas preferências e deixe o app fazer o resto.

PLANEJE EM SEGUNDOS
Crie uma semana completa de almoços e jantares com um toque. O preenchimento automático inteligente preenche seu planner com a biblioteca de pratos e regras de etiquetas — sem repetição, sem estresse.

SUA BIBLIOTECA DE PRATOS
Adicione todos os favoritos da casa e marque-os com regras inteligentes. Configure regras como "máximo 2 vezes por semana" ou "só jantar" e o MealMood as respeitará automaticamente.

EXPORTAÇÕES LINDAS — PRONTAS PARA A GELADEIRA
Compartilhe seu cardápio semanal como imagem em 3 estilos:
• Grade clássica
• Elegante grade escolar
• Retrato A4 vertical — imprima e cole na geladeira

SEMPRE SINCRONIZADO
O iCloud mantém seu plano atualizado em todos os seus dispositivos Apple. Um plano, visível em todo lugar.

INTEGRAÇÃO COM CALENDÁRIO
Sincronize suas refeições diretamente com o Calendário Apple para que toda a família saiba sempre o que tem para comer.

— — —

GRATUITO
• Até 20 pratos
• Planejamento da semana atual
• Com anúncios

PREMIUM (assinatura mensal)
• Pratos ilimitados
• Planeje qualquer semana futura
• Regras de etiquetas avançadas
• Exportações do cardápio semanal
• Sem anúncios

— — —

Disponível em português, espanhol, inglês, francês, alemão e italiano. +
+
+ + + +
+
Changelog
+ Planejamento mais inteligente nesta atualização:

• O preenchimento automático agora evita repetir o mesmo prato duas vezes quando há alternativas disponíveis — semanas mais variadas e equilibradas.
• Adicionar vários pratos é mais rápido: o formulário permanece aberto após salvar para adicionar vários seguidos.
• Sincronização com o calendário corrigida: os eventos agora são atualizados em vez de duplicados ao sincronizar novamente.
• Configuração simplificada: os pratos agora são opcionais durante a configuração inicial — você pode adicioná-los depois, se preferir.

Melhorias gerais e correções de bugs.
+
+ + + +
+
Promotional Text
+ Planeje almoços e jantares de toda a semana em segundos. 3 estilos de exportação — incluindo A4 para a geladeira. Grátis para começar. +
+ + +
+
Screenshots
+ + + + + + + +
+ +
+ + + + +
+
Review Information
+
+ +
+ First name +
+
+ Alexandre
+
+ +
+ Last name +
+
+ Vazquez
+
+ +
+ Phone number +
+
+ +34683619601
+
+ +
+ Email address +
+
+ alexandre.vazquez@gmail.com
+
+ +
+ Demo user +
+
+
+
+ +
+ Demo password +
+
+
+
+ +
+ Notes +
+
+ Please review together with IAP https://appstoreconnect.apple.com/apps/6759255553/distribution/iaps/6759257107 that is also new for this version.

MealMood includes one auto-renewable subscription IAP (MealMood Premium Monthly, 2.99 EUR/month) that is also new in this version. So please review both together.

What is unlocked by Premium:

Ad-free experience
Monthly history view
Week plan image export/share
Advanced planning features
How to test IAP:

Launch app and open Settings > Premium (or tap a Premium-locked action such as Export when on Free).
Tap Subscribe to start the purchase flow.
Tap Restore Purchases to verify restoration behavior.
Family Sharing is enabled for this subscription in App Store Connect.
Notes:

The app is fully usable without purchase (Free plan).
Premium paywall is only shown when accessing Premium features.
No external payment methods are used.
+
+ +
+
+ + + diff --git a/fastlane/README.md b/fastlane/README.md new file mode 100644 index 0000000..72db873 --- /dev/null +++ b/fastlane/README.md @@ -0,0 +1,40 @@ +fastlane documentation +---- + +# Installation + +Make sure you have the latest version of the Xcode command line tools installed: + +```sh +xcode-select --install +``` + +For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) + +# Available Actions + +## iOS + +### ios beta + +```sh +[bundle exec] fastlane ios beta +``` + +Push a new beta build to TestFlight + +### ios release + +```sh +[bundle exec] fastlane ios release +``` + +Push a new release build to the App Store + +---- + +This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. + +More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). + +The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). diff --git a/fastlane/metadata/copyright.txt b/fastlane/metadata/copyright.txt new file mode 100644 index 0000000..2178ba4 --- /dev/null +++ b/fastlane/metadata/copyright.txt @@ -0,0 +1 @@ +© 2026 MealMood diff --git a/fastlane/metadata/de-DE/description.txt b/fastlane/metadata/de-DE/description.txt new file mode 100644 index 0000000..97e8075 --- /dev/null +++ b/fastlane/metadata/de-DE/description.txt @@ -0,0 +1,39 @@ +Plane die Mahlzeiten deiner Familie in Minuten — nicht in Stunden. + +MealMood ist der einfache, schöne Wochenmenü-Planer für vielbeschäftigte Familien. Füge deine Lieblingsgerichte hinzu, stelle deine Vorlieben ein und lass die App den Rest erledigen. + +IN SEKUNDEN PLANEN +Erstelle eine vollständige Woche mit Mittag- und Abendessen auf Knopfdruck. Die intelligente Autovervollständigung füllt deinen Planer mit deiner Gerichtebibliothek und Tag-Regeln — ohne Wiederholungen, ohne Stress. + +DEINE GERICHTEBIBLIOTHEK +Füge alle Haushaltsfavoriten hinzu und beschrifte sie mit smarten Tags. Stelle Regeln wie „maximal 2 Mal pro Woche" oder „nur Abendessen" ein — MealMood hält sie automatisch ein. + +WUNDERSCHÖNE EXPORTS — KÜHLSCHRANKFERTIG +Teile deinen Wochenplan als Bild in 3 Stilen: +• Klassisches Raster +• Eleganter Stundenplan-Stil +• Hochformat A4 — ausdrucken und an den Kühlschrank hängen + +IMMER SYNCHRONISIERT +iCloud hält deinen Plan auf allen Apple-Geräten aktuell. Ein Plan, überall sichtbar. + +KALENDERINTEGRATION +Synchronisiere Mahlzeiten direkt mit dem Apple-Kalender, damit die ganze Familie immer weiß, was es zu essen gibt. + +— — — + +KOSTENLOS +• Bis zu 20 Gerichte +• Planung der aktuellen Woche +• Mit Werbung + +PREMIUM (Monatsabo) +• Unbegrenzte Gerichte +• Jede zukünftige Woche planen +• Erweiterte Tag-Regeln +• Wöchentliche Menü-Exports +• Werbefrei + +— — — + +Verfügbar auf Deutsch, Spanisch, Englisch, Französisch, Italienisch und Portugiesisch. \ No newline at end of file diff --git a/fastlane/metadata/de-DE/keywords.txt b/fastlane/metadata/de-DE/keywords.txt new file mode 100644 index 0000000..91cd269 --- /dev/null +++ b/fastlane/metadata/de-DE/keywords.txt @@ -0,0 +1 @@ +Wochenplan,was kochen,Menüplan,Rezepte,Speiseplan,Abendessen Ideen,Mittagessen Ideen,Mahlzeiten \ No newline at end of file diff --git a/fastlane/metadata/de-DE/name.txt b/fastlane/metadata/de-DE/name.txt new file mode 100644 index 0000000..02908ad --- /dev/null +++ b/fastlane/metadata/de-DE/name.txt @@ -0,0 +1 @@ +MealMood: Wochenmenü-Planer \ No newline at end of file diff --git a/fastlane/metadata/de-DE/promotional_text.txt b/fastlane/metadata/de-DE/promotional_text.txt new file mode 100644 index 0000000..dde79f7 --- /dev/null +++ b/fastlane/metadata/de-DE/promotional_text.txt @@ -0,0 +1 @@ +Plane Mittag- und Abendessen für die ganze Woche in Sekunden. 3 Export-Stile — inkl. A4 für den Kühlschrank. Kostenlos starten. \ No newline at end of file diff --git a/fastlane/metadata/de-DE/release_notes.txt b/fastlane/metadata/de-DE/release_notes.txt index dc1b0c4..5ad4747 100644 --- a/fastlane/metadata/de-DE/release_notes.txt +++ b/fastlane/metadata/de-DE/release_notes.txt @@ -1,8 +1,6 @@ -Intelligentere Planung in diesem Update: +Neu in Version 1.0.5: -• Die Auto-Vervollständigung vermeidet jetzt die Wiederholung desselben Gerichts, wenn Alternativen verfügbar sind — abwechslungsreichere und ausgewogenere Wochen. -• Mehrere Gerichte hinzufügen geht jetzt schneller: Das Formular bleibt nach dem Speichern offen, damit du mehrere hintereinander hinzufügen kannst. -• Kalendersynchronisation korrigiert: Ereignisse werden jetzt aktualisiert statt dupliziert, wenn erneut synchronisiert wird. -• Einrichtung vereinfacht: Gerichte sind beim ersten Setup jetzt optional — du kannst sie später hinzufügen, wenn du möchtest. - -Allgemeine Verbesserungen und Fehlerbehebungen. +• Auswärts essen — markiere jeden Slot als „auswärts", damit er nicht als leer gilt und beim Auto-Vervollständigen übersprungen wird. +• Regelkonflikte — sieh alle Regelverstöße auf einen Blick und behebe oder ignoriere sie mit einem Tipp. +• Firebase Crashlytics — Absturzberichte jetzt aktiv für schnellere Fehlerbehebung. +• Suchleisten-Fix — Text ist jetzt im Dunkelmodus lesbar. diff --git a/fastlane/metadata/de-DE/subtitle.txt b/fastlane/metadata/de-DE/subtitle.txt new file mode 100644 index 0000000..7c02d7f --- /dev/null +++ b/fastlane/metadata/de-DE/subtitle.txt @@ -0,0 +1 @@ +Planen, teilen und ausdrucken \ No newline at end of file diff --git a/fastlane/metadata/de-DE/support_url.txt b/fastlane/metadata/de-DE/support_url.txt new file mode 100644 index 0000000..f6a30fa --- /dev/null +++ b/fastlane/metadata/de-DE/support_url.txt @@ -0,0 +1 @@ +https://mealmood.app diff --git a/fastlane/metadata/en-US/apple_tv_privacy_policy.txt b/fastlane/metadata/en-US/apple_tv_privacy_policy.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/fastlane/metadata/en-US/apple_tv_privacy_policy.txt @@ -0,0 +1 @@ + diff --git a/fastlane/metadata/en-US/description.txt b/fastlane/metadata/en-US/description.txt new file mode 100644 index 0000000..b4601e9 --- /dev/null +++ b/fastlane/metadata/en-US/description.txt @@ -0,0 +1,39 @@ +Plan your family's weekly meals in minutes — not hours. + +MealMood is the simple, beautiful weekly meal planner for busy families. Build your dish library, plan lunches and dinners for the week, and share a gorgeous printable menu — straight to the fridge. + +PLAN IN SECONDS +Create a full week of lunches and dinners with one tap. MealMood's smart auto-complete fills your planner using your dish library and tag rules — no repetition, no stress. + +YOUR DISH LIBRARY +Add all your household favorites and label them with smart tags. Set rules like "max 2 times per week" or "dinner only" and MealMood will respect them automatically when auto-completing your week. + +BEAUTIFUL EXPORTS — FRIDGE-READY +Share your weekly menu as a gorgeous image in 3 styles: +• Classic grid layout +• Elegant school timetable style +• Vertical A4 portrait — print it and stick it on the fridge + +ALWAYS IN SYNC +iCloud sync keeps your plan up to date across all your Apple devices. One plan, visible everywhere. + +CALENDAR INTEGRATION +Sync your meals directly to Apple Calendar so the whole family always knows what's for lunch and dinner. + +— — — + +FREE PLAN +• Up to 20 dishes +• Plan the current week +• Ad-supported + +PREMIUM (monthly subscription) +• Unlimited dishes +• Plan any future week +• Advanced tag rules +• Beautiful week exports +• Ad-free experience + +— — — + +Available in English, Spanish, French, German, Italian and Portuguese. \ No newline at end of file diff --git a/fastlane/metadata/en-US/keywords.txt b/fastlane/metadata/en-US/keywords.txt new file mode 100644 index 0000000..8cf4dc9 --- /dev/null +++ b/fastlane/metadata/en-US/keywords.txt @@ -0,0 +1 @@ +dinner ideas,what to cook,weekly menu,lunch ideas,meal prep,food schedule,recipe organizer,cooking \ No newline at end of file diff --git a/fastlane/metadata/en-US/marketing_url.txt b/fastlane/metadata/en-US/marketing_url.txt new file mode 100644 index 0000000..f6a30fa --- /dev/null +++ b/fastlane/metadata/en-US/marketing_url.txt @@ -0,0 +1 @@ +https://mealmood.app diff --git a/fastlane/metadata/en-US/name.txt b/fastlane/metadata/en-US/name.txt new file mode 100644 index 0000000..b5c0250 --- /dev/null +++ b/fastlane/metadata/en-US/name.txt @@ -0,0 +1 @@ +MealMood: Family Meal Planner diff --git a/fastlane/metadata/en-US/privacy_url.txt b/fastlane/metadata/en-US/privacy_url.txt new file mode 100644 index 0000000..69a03f3 --- /dev/null +++ b/fastlane/metadata/en-US/privacy_url.txt @@ -0,0 +1 @@ +https://mealmood.app/privacy.html diff --git a/fastlane/metadata/en-US/promotional_text.txt b/fastlane/metadata/en-US/promotional_text.txt new file mode 100644 index 0000000..8f24b19 --- /dev/null +++ b/fastlane/metadata/en-US/promotional_text.txt @@ -0,0 +1 @@ +Plan lunches & dinners for the whole week in seconds. 3 beautiful export styles — including A4 for the fridge. Free to start, Premium unlocks everything. \ No newline at end of file diff --git a/fastlane/metadata/en-US/release_notes.txt b/fastlane/metadata/en-US/release_notes.txt index 8374569..2963dba 100644 --- a/fastlane/metadata/en-US/release_notes.txt +++ b/fastlane/metadata/en-US/release_notes.txt @@ -1,8 +1,6 @@ -Smarter meal planning in this update: +New in 1.0.5: -• Auto-assign now avoids repeating the same dish twice when alternatives are available — smarter, more varied weekly plans. -• Adding multiple dishes is now faster: the form stays open after saving so you can add several in a row. -• Calendar sync fixed: events are now updated instead of duplicated when re-syncing. -• Onboarding simplified: dishes are now optional during setup — jump in and add them later if you prefer. - -Various improvements and bug fixes. +• Eating out — mark any meal slot as "eating out" so it doesn't count as missing and is skipped by auto-assign. +• Rule conflicts panel — see all rule violations in one place and fix or ignore them with one tap. +• Firebase Crashlytics — crash reporting now active for faster bug fixes. +• Search bar fix — text is now readable in dark mode. diff --git a/fastlane/metadata/en-US/subtitle.txt b/fastlane/metadata/en-US/subtitle.txt new file mode 100644 index 0000000..79d7053 --- /dev/null +++ b/fastlane/metadata/en-US/subtitle.txt @@ -0,0 +1 @@ +Plan Your Meals, Share & Print \ No newline at end of file diff --git a/fastlane/metadata/en-US/support_url.txt b/fastlane/metadata/en-US/support_url.txt new file mode 100644 index 0000000..f6a30fa --- /dev/null +++ b/fastlane/metadata/en-US/support_url.txt @@ -0,0 +1 @@ +https://mealmood.app diff --git a/fastlane/metadata/es-ES/description.txt b/fastlane/metadata/es-ES/description.txt new file mode 100644 index 0000000..8cf15c3 --- /dev/null +++ b/fastlane/metadata/es-ES/description.txt @@ -0,0 +1,39 @@ +Planifica las comidas de la semana en minutos, no en horas. + +MealMood es el planificador de menú semanal simple y bonito para familias ocupadas. Añade tus platos favoritos, configura tus preferencias y deja que la app haga el resto. + +PLANIFICA EN SEGUNDOS +Crea una semana completa de comidas y cenas con un toque. El autocompletado inteligente rellena tu planificador usando tu biblioteca de platos y reglas de etiquetas — sin repeticiones, sin estrés. + +TU BIBLIOTECA DE PLATOS +Añade todos los favoritos de casa y etiquétalos con reglas inteligentes. Configura reglas como "máximo 2 veces por semana" o "solo cenas" y MealMood las respetará automáticamente. + +EXPORTACIONES PRECIOSAS — LISTAS PARA LA NEVERA +Comparte tu menú semanal como imagen en 3 estilos: +• Cuadrícula clásica +• Horario escolar elegante +• Retrato A4 vertical — imprímelo y ponlo en la nevera + +SIEMPRE SINCRONIZADO +iCloud mantiene tu plan actualizado en todos tus dispositivos Apple. Un plan, visible en todas partes. + +INTEGRACIÓN CON CALENDARIO +Sincroniza tus comidas directamente con el Calendario de Apple para que toda la familia sepa siempre qué hay de comer. + +— — — + +PLAN GRATUITO +• Hasta 20 platos +• Planificación de la semana actual +• Con anuncios + +PREMIUM (suscripción mensual) +• Platos ilimitados +• Planifica cualquier semana futura +• Reglas de etiquetas avanzadas +• Exportaciones del menú semanal +• Sin anuncios + +— — — + +Disponible en español, inglés, francés, alemán, italiano y portugués. \ No newline at end of file diff --git a/fastlane/metadata/es-ES/keywords.txt b/fastlane/metadata/es-ES/keywords.txt new file mode 100644 index 0000000..7043104 --- /dev/null +++ b/fastlane/metadata/es-ES/keywords.txt @@ -0,0 +1 @@ +menú semanal,qué cocinar hoy,ideas cenas,organizar comidas,planificar semana,comida semana \ No newline at end of file diff --git a/fastlane/metadata/es-ES/name.txt b/fastlane/metadata/es-ES/name.txt new file mode 100644 index 0000000..dcb885c --- /dev/null +++ b/fastlane/metadata/es-ES/name.txt @@ -0,0 +1 @@ +MealMood: Planificador Semanal \ No newline at end of file diff --git a/fastlane/metadata/es-ES/promotional_text.txt b/fastlane/metadata/es-ES/promotional_text.txt new file mode 100644 index 0000000..b9110c9 --- /dev/null +++ b/fastlane/metadata/es-ES/promotional_text.txt @@ -0,0 +1 @@ +Planifica comidas y cenas de toda la semana en segundos. 3 estilos de exportación — incluido A4 para la nevera. Gratis para empezar. \ No newline at end of file diff --git a/fastlane/metadata/es-ES/release_notes.txt b/fastlane/metadata/es-ES/release_notes.txt index 0a04c24..d2335ae 100644 --- a/fastlane/metadata/es-ES/release_notes.txt +++ b/fastlane/metadata/es-ES/release_notes.txt @@ -1,8 +1,6 @@ -Planificación más inteligente en esta actualización: +Novedades en 1.0.5: -• El auto-completar ahora evita repetir el mismo plato dos veces si hay alternativas — semanas más variadas y equilibradas. -• Añadir varios platos es más rápido: el formulario se queda abierto tras guardar para poder añadir varios seguidos. -• Sincronización con el calendario corregida: los eventos ahora se actualizan en lugar de duplicarse al volver a sincronizar. -• Incorporación simplificada: los platos ahora son opcionales durante la configuración inicial — puedes añadirlos después si lo prefieres. - -Mejoras generales y corrección de errores. +• Comer fuera — marca cualquier hueco como "comida fuera" para que no cuente como vacío y el auto-completar lo omita. +• Panel de conflictos — ve todas las infracciones de reglas juntas y corrígelas o ignóralas con un toque. +• Firebase Crashlytics — informes de errores activados para correcciones más rápidas. +• Corrección barra de búsqueda — el texto ahora se lee correctamente en modo oscuro. diff --git a/fastlane/metadata/es-ES/subtitle.txt b/fastlane/metadata/es-ES/subtitle.txt new file mode 100644 index 0000000..fab7ac2 --- /dev/null +++ b/fastlane/metadata/es-ES/subtitle.txt @@ -0,0 +1 @@ +Planifica, comparte e imprime \ No newline at end of file diff --git a/fastlane/metadata/es-ES/support_url.txt b/fastlane/metadata/es-ES/support_url.txt new file mode 100644 index 0000000..f6a30fa --- /dev/null +++ b/fastlane/metadata/es-ES/support_url.txt @@ -0,0 +1 @@ +https://mealmood.app diff --git a/fastlane/metadata/fr-FR/description.txt b/fastlane/metadata/fr-FR/description.txt new file mode 100644 index 0000000..5c94659 --- /dev/null +++ b/fastlane/metadata/fr-FR/description.txt @@ -0,0 +1,39 @@ +Planifiez les repas de la semaine en quelques minutes, pas en heures. + +MealMood est le planificateur de menus hebdomadaire simple et beau pour les familles actives. Ajoutez vos plats préférés, définissez vos préférences et laissez l'app faire le reste. + +PLANIFIEZ EN QUELQUES SECONDES +Créez une semaine complète de déjeuners et dîners en un tap. La saisie automatique intelligente remplit votre planning avec votre bibliothèque de plats et vos règles d'étiquettes — sans répétition, sans stress. + +VOTRE BIBLIOTHÈQUE DE PLATS +Ajoutez tous vos favoris et étiquetez-les avec des règles intelligentes. Définissez des règles comme « maximum 2 fois par semaine » ou « dîner uniquement » et MealMood les respectera automatiquement. + +EXPORTS MAGNIFIQUES — PRÊTS POUR LE FRIGO +Partagez votre menu en image dans 3 styles : +• Grille classique +• Emploi du temps élégant +• Portrait A4 vertical — imprimez-le et collez-le sur le frigo + +TOUJOURS SYNCHRONISÉ +iCloud maintient votre planning à jour sur tous vos appareils Apple. Un plan, visible partout. + +INTÉGRATION CALENDRIER +Synchronisez vos repas directement dans Calendrier Apple pour que toute la famille sache toujours ce qu'il y a à manger. + +— — — + +GRATUIT +• Jusqu'à 20 plats +• Planification de la semaine en cours +• Avec publicités + +PREMIUM (abonnement mensuel) +• Plats illimités +• Planifiez n'importe quelle semaine future +• Règles d'étiquettes avancées +• Exports du menu hebdomadaire +• Sans publicité + +— — — + +Disponible en français, espagnol, anglais, allemand, italien et portugais. \ No newline at end of file diff --git a/fastlane/metadata/fr-FR/keywords.txt b/fastlane/metadata/fr-FR/keywords.txt new file mode 100644 index 0000000..5e67849 --- /dev/null +++ b/fastlane/metadata/fr-FR/keywords.txt @@ -0,0 +1 @@ +menu semaine,idées dîner,quoi manger,organisation repas,agenda repas,planning dîners,recettes \ No newline at end of file diff --git a/fastlane/metadata/fr-FR/name.txt b/fastlane/metadata/fr-FR/name.txt new file mode 100644 index 0000000..b63597c --- /dev/null +++ b/fastlane/metadata/fr-FR/name.txt @@ -0,0 +1 @@ +MealMood: Planificateur Repas \ No newline at end of file diff --git a/fastlane/metadata/fr-FR/promotional_text.txt b/fastlane/metadata/fr-FR/promotional_text.txt new file mode 100644 index 0000000..f0ba73a --- /dev/null +++ b/fastlane/metadata/fr-FR/promotional_text.txt @@ -0,0 +1 @@ +Planifiez déjeuners et dîners pour toute la semaine en secondes. 3 styles d'export — dont le A4 pour le frigo. Gratuit pour commencer. \ No newline at end of file diff --git a/fastlane/metadata/fr-FR/release_notes.txt b/fastlane/metadata/fr-FR/release_notes.txt index 9db5542..e742522 100644 --- a/fastlane/metadata/fr-FR/release_notes.txt +++ b/fastlane/metadata/fr-FR/release_notes.txt @@ -1,8 +1,6 @@ -Une planification plus intelligente dans cette mise à jour : +Nouveautés dans la version 1.0.5 : -• L'auto-complétion évite désormais de répéter le même plat deux fois lorsqu'il y a des alternatives — des semaines plus variées et équilibrées. -• Ajouter plusieurs plats est plus rapide : le formulaire reste ouvert après l'enregistrement pour en ajouter plusieurs à la suite. -• Synchronisation du calendrier corrigée : les événements sont maintenant mis à jour au lieu d'être dupliqués lors d'une nouvelle synchronisation. -• Démarrage simplifié : les plats sont désormais optionnels pendant la configuration initiale — vous pouvez les ajouter plus tard si vous préférez. - -Améliorations générales et corrections de bugs. +• Repas dehors — marquez n'importe quel créneau comme « repas dehors » pour qu'il ne compte pas comme vide et soit ignoré par l'auto-complétion. +• Panneau de conflits — visualisez toutes les infractions aux règles en un seul endroit et corrigez-les ou ignorez-les en un tap. +• Firebase Crashlytics — rapports de plantages activés pour des corrections plus rapides. +• Correction barre de recherche — le texte est maintenant lisible en mode sombre. diff --git a/fastlane/metadata/fr-FR/subtitle.txt b/fastlane/metadata/fr-FR/subtitle.txt new file mode 100644 index 0000000..16e9dd1 --- /dev/null +++ b/fastlane/metadata/fr-FR/subtitle.txt @@ -0,0 +1 @@ +Planifiez, partagez, imprimez \ No newline at end of file diff --git a/fastlane/metadata/fr-FR/support_url.txt b/fastlane/metadata/fr-FR/support_url.txt new file mode 100644 index 0000000..f6a30fa --- /dev/null +++ b/fastlane/metadata/fr-FR/support_url.txt @@ -0,0 +1 @@ +https://mealmood.app diff --git a/fastlane/metadata/it/description.txt b/fastlane/metadata/it/description.txt new file mode 100644 index 0000000..b07b41b --- /dev/null +++ b/fastlane/metadata/it/description.txt @@ -0,0 +1,39 @@ +Pianifica i pasti della famiglia in pochi minuti, non in ore. + +MealMood è il pianificatore di menu settimanale semplice e bello per le famiglie indaffarate. Aggiungi i tuoi piatti preferiti, imposta le tue preferenze e lascia che l'app faccia il resto. + +PIANIFICA IN SECONDI +Crea una settimana completa di pranzi e cene con un tocco. Il completamento automatico intelligente riempie il tuo planner con la libreria di piatti e le regole dei tag — niente ripetizioni, niente stress. + +LA TUA LIBRERIA DI PIATTI +Aggiungi tutti i preferiti di casa e etichettali con regole intelligenti. Imposta regole come "massimo 2 volte a settimana" o "solo cena" e MealMood le rispetterà automaticamente. + +EXPORT BELLISSIMI — PRONTI PER IL FRIGO +Condividi il tuo menu settimanale come immagine in 3 stili: +• Griglia classica +• Elegante orario scolastico +• Ritratto A4 verticale — stampalo e mettilo sul frigorifero + +SEMPRE SINCRONIZZATO +iCloud mantiene il tuo piano aggiornato su tutti i dispositivi Apple. Un piano, visibile ovunque. + +INTEGRAZIONE CALENDARIO +Sincronizza i pasti direttamente con Calendario Apple così tutta la famiglia sa sempre cosa c'è da mangiare. + +— — — + +GRATUITO +• Fino a 20 piatti +• Pianificazione della settimana corrente +• Con pubblicità + +PREMIUM (abbonamento mensile) +• Piatti illimitati +• Pianifica qualsiasi settimana futura +• Regole tag avanzate +• Export menu settimanale +• Senza pubblicità + +— — — + +Disponibile in italiano, spagnolo, inglese, francese, tedesco e portoghese. \ No newline at end of file diff --git a/fastlane/metadata/it/keywords.txt b/fastlane/metadata/it/keywords.txt new file mode 100644 index 0000000..2c06d47 --- /dev/null +++ b/fastlane/metadata/it/keywords.txt @@ -0,0 +1 @@ +menu settimanale,idee cena,cosa cucinare,organizzare pasti,pranzo cena,ricette,programma pasti \ No newline at end of file diff --git a/fastlane/metadata/it/name.txt b/fastlane/metadata/it/name.txt new file mode 100644 index 0000000..9ce851a --- /dev/null +++ b/fastlane/metadata/it/name.txt @@ -0,0 +1 @@ +MealMood: Pianificatore Pasti \ No newline at end of file diff --git a/fastlane/metadata/it/promotional_text.txt b/fastlane/metadata/it/promotional_text.txt new file mode 100644 index 0000000..6893796 --- /dev/null +++ b/fastlane/metadata/it/promotional_text.txt @@ -0,0 +1 @@ +Pianifica pranzi e cene per tutta la settimana in secondi. 3 stili di export — incluso A4 per il frigo. Gratis per iniziare. \ No newline at end of file diff --git a/fastlane/metadata/it/release_notes.txt b/fastlane/metadata/it/release_notes.txt index 08ee749..647e0f2 100644 --- a/fastlane/metadata/it/release_notes.txt +++ b/fastlane/metadata/it/release_notes.txt @@ -1,8 +1,6 @@ -Pianificazione più intelligente in questo aggiornamento: +Novità nella versione 1.0.5: -• Il completamento automatico ora evita di ripetere lo stesso piatto due volte quando ci sono alternative — settimane più varie ed equilibrate. -• Aggiungere più piatti è più veloce: il modulo rimane aperto dopo il salvataggio per poterne aggiungere più di uno di seguito. -• Sincronizzazione del calendario corretta: gli eventi vengono ora aggiornati invece di essere duplicati quando si risincronizza. -• Configurazione semplificata: i piatti sono ora opzionali durante la configurazione iniziale — puoi aggiungerli dopo se preferisci. - -Miglioramenti generali e correzioni di bug. +• Mangiare fuori — segna qualsiasi slot come "fuori casa" per non contarlo come vuoto e ignorarlo nell'auto-completamento. +• Pannello conflitti — visualizza tutte le violazioni delle regole in un unico posto e correggile o ignorale con un tap. +• Firebase Crashlytics — report degli arresti anomali attivi per correzioni più rapide. +• Correzione barra di ricerca — il testo ora è leggibile in modalità scura. diff --git a/fastlane/metadata/it/subtitle.txt b/fastlane/metadata/it/subtitle.txt new file mode 100644 index 0000000..102aadb --- /dev/null +++ b/fastlane/metadata/it/subtitle.txt @@ -0,0 +1 @@ +Pianifica, condividi, stampa \ No newline at end of file diff --git a/fastlane/metadata/it/support_url.txt b/fastlane/metadata/it/support_url.txt new file mode 100644 index 0000000..f6a30fa --- /dev/null +++ b/fastlane/metadata/it/support_url.txt @@ -0,0 +1 @@ +https://mealmood.app diff --git a/fastlane/metadata/primary_category.txt b/fastlane/metadata/primary_category.txt new file mode 100644 index 0000000..90b0c7b --- /dev/null +++ b/fastlane/metadata/primary_category.txt @@ -0,0 +1 @@ +FOOD_AND_DRINK diff --git a/fastlane/metadata/primary_first_sub_category.txt b/fastlane/metadata/primary_first_sub_category.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/fastlane/metadata/primary_first_sub_category.txt @@ -0,0 +1 @@ + diff --git a/fastlane/metadata/primary_second_sub_category.txt b/fastlane/metadata/primary_second_sub_category.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/fastlane/metadata/primary_second_sub_category.txt @@ -0,0 +1 @@ + diff --git a/fastlane/metadata/pt-BR/description.txt b/fastlane/metadata/pt-BR/description.txt new file mode 100644 index 0000000..c132cee --- /dev/null +++ b/fastlane/metadata/pt-BR/description.txt @@ -0,0 +1,39 @@ +Planeje as refeições da família em minutos — não em horas. + +MealMood é o planejador de cardápio semanal simples e bonito para famílias ocupadas. Adicione seus pratos favoritos, configure suas preferências e deixe o app fazer o resto. + +PLANEJE EM SEGUNDOS +Crie uma semana completa de almoços e jantares com um toque. O preenchimento automático inteligente preenche seu planner com a biblioteca de pratos e regras de etiquetas — sem repetição, sem estresse. + +SUA BIBLIOTECA DE PRATOS +Adicione todos os favoritos da casa e marque-os com regras inteligentes. Configure regras como "máximo 2 vezes por semana" ou "só jantar" e o MealMood as respeitará automaticamente. + +EXPORTAÇÕES LINDAS — PRONTAS PARA A GELADEIRA +Compartilhe seu cardápio semanal como imagem em 3 estilos: +• Grade clássica +• Elegante grade escolar +• Retrato A4 vertical — imprima e cole na geladeira + +SEMPRE SINCRONIZADO +O iCloud mantém seu plano atualizado em todos os seus dispositivos Apple. Um plano, visível em todo lugar. + +INTEGRAÇÃO COM CALENDÁRIO +Sincronize suas refeições diretamente com o Calendário Apple para que toda a família saiba sempre o que tem para comer. + +— — — + +GRATUITO +• Até 20 pratos +• Planejamento da semana atual +• Com anúncios + +PREMIUM (assinatura mensal) +• Pratos ilimitados +• Planeje qualquer semana futura +• Regras de etiquetas avançadas +• Exportações do cardápio semanal +• Sem anúncios + +— — — + +Disponível em português, espanhol, inglês, francês, alemão e italiano. \ No newline at end of file diff --git a/fastlane/metadata/pt-BR/keywords.txt b/fastlane/metadata/pt-BR/keywords.txt new file mode 100644 index 0000000..ef99db7 --- /dev/null +++ b/fastlane/metadata/pt-BR/keywords.txt @@ -0,0 +1 @@ +cardápio semanal,o que comer hoje,ideias jantar,organizar refeições,plano semanal,receitas \ No newline at end of file diff --git a/fastlane/metadata/pt-BR/name.txt b/fastlane/metadata/pt-BR/name.txt new file mode 100644 index 0000000..a3b94bc --- /dev/null +++ b/fastlane/metadata/pt-BR/name.txt @@ -0,0 +1 @@ +MealMood: Planejador Semanal \ No newline at end of file diff --git a/fastlane/metadata/pt-BR/promotional_text.txt b/fastlane/metadata/pt-BR/promotional_text.txt new file mode 100644 index 0000000..201f586 --- /dev/null +++ b/fastlane/metadata/pt-BR/promotional_text.txt @@ -0,0 +1 @@ +Planeje almoços e jantares de toda a semana em segundos. 3 estilos de exportação — incluindo A4 para a geladeira. Grátis para começar. \ No newline at end of file diff --git a/fastlane/metadata/pt-BR/release_notes.txt b/fastlane/metadata/pt-BR/release_notes.txt index 786ed49..1aed798 100644 --- a/fastlane/metadata/pt-BR/release_notes.txt +++ b/fastlane/metadata/pt-BR/release_notes.txt @@ -1,8 +1,6 @@ -Planejamento mais inteligente nesta atualização: +Novidades na versão 1.0.5: -• O preenchimento automático agora evita repetir o mesmo prato duas vezes quando há alternativas disponíveis — semanas mais variadas e equilibradas. -• Adicionar vários pratos é mais rápido: o formulário permanece aberto após salvar para adicionar vários seguidos. -• Sincronização com o calendário corrigida: os eventos agora são atualizados em vez de duplicados ao sincronizar novamente. -• Configuração simplificada: os pratos agora são opcionais durante a configuração inicial — você pode adicioná-los depois, se preferir. - -Melhorias gerais e correções de bugs. +• Comer fora — marque qualquer slot como "comer fora" para não contá-lo como vazio e o auto-preenchimento irá pulá-lo. +• Painel de conflitos — veja todas as violações de regras juntas e corrija ou ignore com um toque. +• Firebase Crashlytics — relatórios de falhas agora ativos para correções mais rápidas. +• Correção da barra de busca — o texto agora é legível no modo escuro. diff --git a/fastlane/metadata/pt-BR/subtitle.txt b/fastlane/metadata/pt-BR/subtitle.txt new file mode 100644 index 0000000..19e7844 --- /dev/null +++ b/fastlane/metadata/pt-BR/subtitle.txt @@ -0,0 +1 @@ +Planeje, compartilhe e imprima \ No newline at end of file diff --git a/fastlane/metadata/pt-BR/support_url.txt b/fastlane/metadata/pt-BR/support_url.txt new file mode 100644 index 0000000..f6a30fa --- /dev/null +++ b/fastlane/metadata/pt-BR/support_url.txt @@ -0,0 +1 @@ +https://mealmood.app diff --git a/fastlane/metadata/review_information/demo_password.txt b/fastlane/metadata/review_information/demo_password.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/fastlane/metadata/review_information/demo_password.txt @@ -0,0 +1 @@ + diff --git a/fastlane/metadata/review_information/demo_user.txt b/fastlane/metadata/review_information/demo_user.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/fastlane/metadata/review_information/demo_user.txt @@ -0,0 +1 @@ + diff --git a/fastlane/metadata/review_information/email_address.txt b/fastlane/metadata/review_information/email_address.txt new file mode 100644 index 0000000..91846fb --- /dev/null +++ b/fastlane/metadata/review_information/email_address.txt @@ -0,0 +1 @@ +alexandre.vazquez@gmail.com diff --git a/fastlane/metadata/review_information/first_name.txt b/fastlane/metadata/review_information/first_name.txt new file mode 100644 index 0000000..5da6aa5 --- /dev/null +++ b/fastlane/metadata/review_information/first_name.txt @@ -0,0 +1 @@ +Alexandre diff --git a/fastlane/metadata/review_information/last_name.txt b/fastlane/metadata/review_information/last_name.txt new file mode 100644 index 0000000..0a5f27a --- /dev/null +++ b/fastlane/metadata/review_information/last_name.txt @@ -0,0 +1 @@ +Vazquez diff --git a/fastlane/metadata/review_information/notes.txt b/fastlane/metadata/review_information/notes.txt new file mode 100644 index 0000000..b5ee0ee --- /dev/null +++ b/fastlane/metadata/review_information/notes.txt @@ -0,0 +1,21 @@ +Please review together with IAP https://appstoreconnect.apple.com/apps/6759255553/distribution/iaps/6759257107 that is also new for this version. + +MealMood includes one auto-renewable subscription IAP (MealMood Premium Monthly, 2.99 EUR/month) that is also new in this version. So please review both together. + +What is unlocked by Premium: + +Ad-free experience +Monthly history view +Week plan image export/share +Advanced planning features +How to test IAP: + +Launch app and open Settings > Premium (or tap a Premium-locked action such as Export when on Free). +Tap Subscribe to start the purchase flow. +Tap Restore Purchases to verify restoration behavior. +Family Sharing is enabled for this subscription in App Store Connect. +Notes: + +The app is fully usable without purchase (Free plan). +Premium paywall is only shown when accessing Premium features. +No external payment methods are used. diff --git a/fastlane/metadata/review_information/phone_number.txt b/fastlane/metadata/review_information/phone_number.txt new file mode 100644 index 0000000..adfe63f --- /dev/null +++ b/fastlane/metadata/review_information/phone_number.txt @@ -0,0 +1 @@ ++34683619601 diff --git a/fastlane/metadata/secondary_category.txt b/fastlane/metadata/secondary_category.txt new file mode 100644 index 0000000..cc52552 --- /dev/null +++ b/fastlane/metadata/secondary_category.txt @@ -0,0 +1 @@ +LIFESTYLE diff --git a/fastlane/metadata/secondary_first_sub_category.txt b/fastlane/metadata/secondary_first_sub_category.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/fastlane/metadata/secondary_first_sub_category.txt @@ -0,0 +1 @@ + diff --git a/fastlane/metadata/secondary_second_sub_category.txt b/fastlane/metadata/secondary_second_sub_category.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/fastlane/metadata/secondary_second_sub_category.txt @@ -0,0 +1 @@ + diff --git a/fastlane/report.xml b/fastlane/report.xml new file mode 100644 index 0000000..040bc52 --- /dev/null +++ b/fastlane/report.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fastlane/screenshots/README.txt b/fastlane/screenshots/README.txt new file mode 100644 index 0000000..948c580 --- /dev/null +++ b/fastlane/screenshots/README.txt @@ -0,0 +1,30 @@ +## Screenshots Naming Rules + +Put all screenshots you want to use inside the folder of its language (e.g. `en-US`). +The device type will automatically be recognized using the image resolution. + +The screenshots can be named whatever you want, but keep in mind they are sorted +alphabetically, in a human-friendly way. See https://github.com/fastlane/fastlane/pull/18200 for more details. + +### Exceptions + +#### iPad Pro (3rd Gen) 12.9" + +Since iPad Pro (3rd Gen) 12.9" and iPad Pro (2nd Gen) 12.9" have the same image +resolution, screenshots of the iPad Pro (3rd gen) 12.9" must contain either the +string `iPad Pro (12.9-inch) (3rd generation)`, `IPAD_PRO_3GEN_129`, or `ipadPro129` +(App Store Connect's internal naming of the display family for the 3rd generation iPad Pro) +in its filename to be assigned the correct display family and to be uploaded to +the correct screenshot slot in your app's metadata. + +### Other Platforms + +#### Apple TV + +Apple TV screenshots should be stored in a subdirectory named `appleTV` with language +folders inside of it. + +#### iMessage + +iMessage screenshots, like the Apple TV ones, should also be stored in a subdirectory +named `iMessage`, with language folders inside of it. diff --git a/fastlane/screenshots/en-US/0_APP_IPAD_PRO_3GEN_129_0.png b/fastlane/screenshots/en-US/0_APP_IPAD_PRO_3GEN_129_0.png new file mode 100644 index 0000000..f846aa9 Binary files /dev/null and b/fastlane/screenshots/en-US/0_APP_IPAD_PRO_3GEN_129_0.png differ diff --git a/fastlane/screenshots/en-US/0_APP_IPHONE_67_0.png b/fastlane/screenshots/en-US/0_APP_IPHONE_67_0.png new file mode 100644 index 0000000..4342e05 Binary files /dev/null and b/fastlane/screenshots/en-US/0_APP_IPHONE_67_0.png differ diff --git a/fastlane/screenshots/en-US/1_APP_IPAD_PRO_3GEN_129_1.png b/fastlane/screenshots/en-US/1_APP_IPAD_PRO_3GEN_129_1.png new file mode 100644 index 0000000..273629e Binary files /dev/null and b/fastlane/screenshots/en-US/1_APP_IPAD_PRO_3GEN_129_1.png differ diff --git a/fastlane/screenshots/en-US/1_APP_IPHONE_67_1.png b/fastlane/screenshots/en-US/1_APP_IPHONE_67_1.png new file mode 100644 index 0000000..d70c77c Binary files /dev/null and b/fastlane/screenshots/en-US/1_APP_IPHONE_67_1.png differ diff --git a/fastlane/screenshots/en-US/2_APP_IPAD_PRO_3GEN_129_2.png b/fastlane/screenshots/en-US/2_APP_IPAD_PRO_3GEN_129_2.png new file mode 100644 index 0000000..de7d57b Binary files /dev/null and b/fastlane/screenshots/en-US/2_APP_IPAD_PRO_3GEN_129_2.png differ diff --git a/fastlane/screenshots/en-US/2_APP_IPHONE_67_2.png b/fastlane/screenshots/en-US/2_APP_IPHONE_67_2.png new file mode 100644 index 0000000..7a1b9ab Binary files /dev/null and b/fastlane/screenshots/en-US/2_APP_IPHONE_67_2.png differ diff --git a/fastlane/screenshots/en-US/3_APP_IPAD_PRO_3GEN_129_3.png b/fastlane/screenshots/en-US/3_APP_IPAD_PRO_3GEN_129_3.png new file mode 100644 index 0000000..1535e00 Binary files /dev/null and b/fastlane/screenshots/en-US/3_APP_IPAD_PRO_3GEN_129_3.png differ diff --git a/fastlane/screenshots/en-US/3_APP_IPHONE_67_3.png b/fastlane/screenshots/en-US/3_APP_IPHONE_67_3.png new file mode 100644 index 0000000..30754c3 Binary files /dev/null and b/fastlane/screenshots/en-US/3_APP_IPHONE_67_3.png differ diff --git a/fastlane/screenshots/en-US/4_APP_IPAD_PRO_3GEN_129_4.png b/fastlane/screenshots/en-US/4_APP_IPAD_PRO_3GEN_129_4.png new file mode 100644 index 0000000..353ff63 Binary files /dev/null and b/fastlane/screenshots/en-US/4_APP_IPAD_PRO_3GEN_129_4.png differ diff --git a/fastlane/screenshots/en-US/4_APP_IPHONE_67_4.png b/fastlane/screenshots/en-US/4_APP_IPHONE_67_4.png new file mode 100644 index 0000000..c141d77 Binary files /dev/null and b/fastlane/screenshots/en-US/4_APP_IPHONE_67_4.png differ