1.1.1 (46): subscription IAP, export compliance, metadata

- Switch premium to AutoRenewable subscription (com.mealmood.premium.monthly.sub);
  legacy one-time purchasers retain lifetime access via StoreManager fallback
- Add ITSAppUsesNonExemptEncryption=false to Info.plist (no custom encryption)
- Update storekit config with both legacy NonConsumable and new subscription
- Bump version 1.1.1 build 46; update fastlane metadata and release notes (6 langs)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
alexandrev-tibco
2026-05-18 11:28:14 +02:00
parent 7da7995538
commit 89b35dc4ae
20 changed files with 74 additions and 86 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
- **Name**: MealMood: Family Meal Planner - **Name**: MealMood: Family Meal Planner
- **Bundle ID**: com.alexandrevazquez.mealmood - **Bundle ID**: com.alexandrevazquez.mealmood
- **Platform**: iOS/iPadOS (also runs as iOS app on Mac) - **Platform**: iOS/iPadOS (also runs as iOS app on Mac)
- **Current version**: 1.0.4 - **Current version**: 1.1.0
- **Main branch**: `1.0.1` (production), feature branches named after versions - **Main branch**: `1.0.1` (production), feature branches named after versions
## Credentials — all managed via `pass` (GPG-encrypted, syncs to Gitea) ## Credentials — all managed via `pass` (GPG-encrypted, syncs to Gitea)
+4 -4
View File
@@ -860,7 +860,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = MealMood/Resources/MealMood.entitlements; CODE_SIGN_ENTITLEMENTS = MealMood/Resources/MealMood.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
CURRENT_PROJECT_VERSION = 31; CURRENT_PROJECT_VERSION = 46;
DEVELOPMENT_TEAM = 2825Q76T7H; DEVELOPMENT_TEAM = 2825Q76T7H;
GENERATE_INFOPLIST_FILE = NO; GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = MealMood/Resources/Info.plist; INFOPLIST_FILE = MealMood/Resources/Info.plist;
@@ -869,7 +869,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.5; MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.alexandrevazquez.mealmood; PRODUCT_BUNDLE_IDENTIFIER = com.alexandrevazquez.mealmood;
PRODUCT_NAME = MealMood; PRODUCT_NAME = MealMood;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -965,7 +965,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = MealMood/Resources/MealMood.entitlements; CODE_SIGN_ENTITLEMENTS = MealMood/Resources/MealMood.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
CURRENT_PROJECT_VERSION = 31; CURRENT_PROJECT_VERSION = 46;
DEVELOPMENT_TEAM = 2825Q76T7H; DEVELOPMENT_TEAM = 2825Q76T7H;
GENERATE_INFOPLIST_FILE = NO; GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = MealMood/Resources/Info.plist; INFOPLIST_FILE = MealMood/Resources/Info.plist;
@@ -974,7 +974,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.5; MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.alexandrevazquez.mealmood; PRODUCT_BUNDLE_IDENTIFIER = com.alexandrevazquez.mealmood;
PRODUCT_NAME = MealMood; PRODUCT_NAME = MealMood;
SDKROOT = iphoneos; SDKROOT = iphoneos;
+16 -2
View File
@@ -14,7 +14,21 @@
], ],
"products" : [ "products" : [
{
"displayPrice" : "2.99",
"familyShareable" : false,
"internalID" : "B2F4D1A0-3E7C-4F8B-9D2E-1A5C6B0E3F7D",
"localizations" : [
{
"description" : "Lifetime access to all premium features.",
"displayName" : "MealMood Premium (Legacy)",
"locale" : "en_US"
}
],
"productID" : "com.mealmood.premium.monthly",
"referenceName" : "MealMood Premium Legacy",
"type" : "NonConsumable"
}
], ],
"settings" : { "settings" : {
"_applicationInternalID" : "2147483647", "_applicationInternalID" : "2147483647",
@@ -60,7 +74,7 @@
"locale" : "en_US" "locale" : "en_US"
} }
], ],
"productID" : "com.mealmood.premium.monthly", "productID" : "com.mealmood.premium.monthly.sub",
"recurringSubscriptionPeriod" : "P1M", "recurringSubscriptionPeriod" : "P1M",
"referenceName" : "MealMood Premium Monthly", "referenceName" : "MealMood Premium Monthly",
"subscriptionPricePointID" : "0" "subscriptionPricePointID" : "0"
+4 -2
View File
@@ -15,11 +15,13 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.0.6</string> <string>1.1.1</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>41</string> <string>46</string>
<key>GADApplicationIdentifier</key> <key>GADApplicationIdentifier</key>
<string>ca-app-pub-1549720748100858~9985112590</string> <string>ca-app-pub-1549720748100858~9985112590</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>NSCalendarsFullAccessUsageDescription</key> <key>NSCalendarsFullAccessUsageDescription</key>
+20 -25
View File
@@ -27,11 +27,13 @@ final class StoreManager: ObservableObject {
@Published var debugLoadedProductIds: [String] = [] @Published var debugLoadedProductIds: [String] = []
@Published var debugLoadedProducts: [String] = [] @Published var debugLoadedProducts: [String] = []
static let monthlyProductId = "com.mealmood.premium.monthly" static let monthlyProductId = "com.mealmood.premium.monthly.sub"
static let legacyOneTimeProductId = "com.mealmood.premium.monthly"
private var productIds: [String] { private var productIds: [String] {
var ids = [Self.monthlyProductId] var ids = [Self.monthlyProductId, Self.legacyOneTimeProductId]
if let bundleId = Bundle.main.bundleIdentifier { if let bundleId = Bundle.main.bundleIdentifier {
ids.append("\(bundleId).premium.monthly.sub")
ids.append("\(bundleId).premium.monthly") ids.append("\(bundleId).premium.monthly")
} }
@@ -115,42 +117,35 @@ final class StoreManager: ObservableObject {
} }
var monthlyProduct: Product? { var monthlyProduct: Product? {
if let match = products.first(where: { product in if let match = products.first(where: { $0.id == Self.monthlyProductId && $0.type == .autoRenewable }) {
product.id == Self.monthlyProductId && product.type == .autoRenewable
}) {
return match return match
} }
if let match = products.first(where: { $0.id == Self.monthlyProductId }) {
return match
}
if let match = products.first(where: { $0.id.contains("monthly") && $0.type == .autoRenewable }) {
return match
}
if let match = products.first(where: { $0.type == .autoRenewable }) { if let match = products.first(where: { $0.type == .autoRenewable }) {
return match return match
} }
if let match = products.first(where: { $0.id == Self.legacyOneTimeProductId }) {
return match
}
return products.first return products.first
} }
var debugProductIds: [String] { productIds } var debugProductIds: [String] { productIds }
static func hasActiveSubscription() async -> Bool { static func hasActiveSubscription() async -> Bool {
// Check all known product ID variants to handle both the canonical ID let bundleId = Bundle.main.bundleIdentifier ?? ""
// and any bundle-prefixed IDs that may have been used at purchase time. let subscriptionIds: Set<String> = [monthlyProductId, "\(bundleId).premium.monthly.sub"]
var knownIds: [String] = [monthlyProductId] // Legacy one-time purchases grant lifetime access no expiration check needed.
if let bundleId = Bundle.main.bundleIdentifier { let legacyIds: Set<String> = [legacyOneTimeProductId, "\(bundleId).premium.monthly"]
knownIds.append("\(bundleId).premium.monthly")
}
let ids = Set(knownIds)
for await result in Transaction.currentEntitlements { for await result in Transaction.currentEntitlements {
if case .verified(let transaction) = result, guard case .verified(let transaction) = result,
ids.contains(transaction.productID), transaction.revocationDate == nil else { continue }
transaction.revocationDate == nil {
if legacyIds.contains(transaction.productID) {
return true
}
if subscriptionIds.contains(transaction.productID) {
if let expirationDate = transaction.expirationDate, expirationDate < Date() { if let expirationDate = transaction.expirationDate, expirationDate < Date() {
continue continue
} }
+2 -2
View File
@@ -15,9 +15,9 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>XPC!</string> <string>XPC!</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.0.6</string> <string>1.1.1</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>41</string> <string>46</string>
<key>NSExtension</key> <key>NSExtension</key>
<dict> <dict>
<key>NSExtensionPointIdentifier</key> <key>NSExtensionPointIdentifier</key>
+1
View File
@@ -1 +1,2 @@
run_precheck_before_submit(false) run_precheck_before_submit(false)
force(true)
+2 -2
View File
@@ -22,7 +22,7 @@ platform :ios do
build_app( build_app(
scheme: "MealMood", scheme: "MealMood",
export_method: "app-store", export_method: "app-store",
sdk: "iphoneos26.4", sdk: "iphoneos26.5",
export_options: EXPORT_OPTIONS export_options: EXPORT_OPTIONS
) )
upload_to_testflight(skip_waiting_for_build_processing: true) upload_to_testflight(skip_waiting_for_build_processing: true)
@@ -33,7 +33,7 @@ platform :ios do
increment_build_number(xcodeproj: "MealMood.xcodeproj") increment_build_number(xcodeproj: "MealMood.xcodeproj")
build_app( build_app(
scheme: "MealMood", scheme: "MealMood",
sdk: "iphoneos26.4", sdk: "iphoneos26.5",
export_options: EXPORT_OPTIONS export_options: EXPORT_OPTIONS
) )
upload_to_app_store(force: true) upload_to_app_store(force: true)
+1 -1
View File
@@ -1 +1 @@
NEU in 1.0.6: Prioritätsgerichte, tagesbasierte Regeln, Gerichte direkt ersetzen und unvollständige Woche exportieren. NEU in 1.1.0: Premium ist jetzt ein Monatsabo. Bestehende Einmalkäufer behalten ihren vollen Zugang automatisch.
+3 -7
View File
@@ -1,8 +1,4 @@
Neu in Version 1.0.6: Neu in Version 1.1.0:
• Prioritätsgerichte — markiere ein Gericht mit einem Stern, damit die Auto-Vervollständigung es zuerst wählt. • Premium ist jetzt ein monatliches Abo — unbegrenzte Gerichte, Planung und Export für 2,99 €/Monat.
Tagesregeln — Tag-Einschränkungen unterstützen jetzt "nur Wochentage" oder "nur Wochenende". Bestehende Einmalkäufer behalten ihren vollen Zugang automatisch — keine Aktion erforderlich.
• Auto-ausfüllen-Schaltfläche — jetzt direkt sichtbar, wenn leere Slots vorhanden sind.
• Tippen zum Ersetzen — tippe auf einen gefüllten Slot, um das Gericht direkt zu tauschen.
• Jederzeit exportieren — teile deine Woche auch wenn sie unvollständig ist; leere Slots zeigen "Noch offen".
• Sonntags-Erinnerung — neue Benachrichtigung zum Planen der Woche vor Montag.
+1 -1
View File
@@ -1 +1 @@
NEW in 1.0.6: Priority dishes, day-based tag rules, tap to replace meals, and export your week even when it's not complete. NEW in 1.1.0: Premium is now a monthly subscription. Existing lifetime purchasers keep full access automatically.
+3 -7
View File
@@ -1,8 +1,4 @@
New in 1.0.6: New in 1.1.0:
• Priority dishes — star a dish to make auto-assign pick it first. • Premium is now a monthly subscription — unlimited dishes, planning, and export for $2.99/month.
Day rules — tag restrictions now support "weekdays only" or "weekend only". Existing lifetime purchasers keep full access automatically, no action needed.
• Auto-assign button — now prominently shown whenever there are empty slots.
• Tap to replace — tap any filled slot to swap the dish directly.
• Export anytime — share your week even if it's not complete yet; empty slots show "TBD".
• Sunday reminder — new notification to plan next week before Monday.
+1 -1
View File
@@ -1 +1 @@
NUEVO en 1.0.6: Platos prioritarios, reglas por días, toca para reemplazar y exporta tu semana aunque no esté completa. NUEVO en 1.1.0: Premium ahora es una suscripción mensual. Los compradores previos conservan su acceso completo automáticamente.
+3 -7
View File
@@ -1,8 +1,4 @@
Novedades en 1.0.6: Novedades en 1.1.0:
• Platos prioritarios — marca un plato con estrella para que el auto-completar lo elija primero. • Premium ahora es una suscripción mensual — platos ilimitados, planificación y exportación por 2,99 €/mes.
Reglas de días — las restricciones de etiquetas ahora admiten "solo entre semana" o "solo fin de semana". Los usuarios con compra única previa conservan el acceso completo de forma automática.
• Botón auto-rellenar — ahora visible directamente cuando hay huecos vacíos.
• Toca para reemplazar — toca un hueco relleno para cambiar el plato directamente.
• Exportar en cualquier momento — comparte tu semana aunque no esté completa; los huecos vacíos muestran "Por definir".
• Recordatorio dominical — nueva notificación para planificar la semana antes del lunes.
+1 -1
View File
@@ -1 +1 @@
NOUVEAU v1.0.6 : Plats prioritaires, règles par jour, remplacer en un tap, et exportez même une semaine incomplète. NOUVEAU v1.1.0 : Premium est désormais un abonnement mensuel. Les acheteurs existants conservent automatiquement leur accès complet.
+3 -7
View File
@@ -1,8 +1,4 @@
Nouveautés dans la version 1.0.6 : Nouveautés dans la version 1.1.0 :
• Plats prioritaires — mettez une étoile à un plat pour que l'auto-complétion le choisisse en premier. • Premium est désormais un abonnement mensuel — plats illimités, planification et export pour 2,99 €/mois.
Règles de jours — les restrictions d'étiquettes prennent désormais en charge "jours de semaine" ou "week-end". Les utilisateurs ayant effectué un achat unique conservent automatiquement leur accès complet.
• Bouton auto-remplir — désormais visible directement lorsqu'il y a des créneaux vides.
• Appuyer pour remplacer — appuyez sur un créneau rempli pour changer le plat directement.
• Exporter à tout moment — partagez votre semaine même incomplète ; les créneaux vides affichent "À définir".
• Rappel du dimanche — nouvelle notification pour planifier la semaine avant le lundi.
+1 -1
View File
@@ -1 +1 @@
NOVITÀ v1.0.6: Piatti prioritari, regole per giorno, sostituisci con un tap ed esporta anche settimane incomplete. NOVITÀ v1.1.0: Premium è ora un abbonamento mensile. Gli acquirenti precedenti mantengono automaticamente l'accesso completo.
+3 -7
View File
@@ -1,8 +1,4 @@
Novità nella versione 1.0.6: Novità nella versione 1.1.0:
• Piatti prioritari — metti una stella a un piatto per farlo scegliere per primo dall'auto-completamento. • Premium è ora un abbonamento mensile — piatti illimitati, pianificazione ed esportazione a 2,99 €/mese.
Regole per giorni — le restrizioni dei tag ora supportano "solo giorni feriali" o "solo weekend". Gli utenti con acquisto singolo precedente mantengono automaticamente l'accesso completo.
• Pulsante auto-assegna — ora visibile direttamente quando ci sono slot vuoti.
• Tocca per sostituire — tocca uno slot pieno per cambiare il piatto direttamente.
• Esporta in qualsiasi momento — condividi la settimana anche se incompleta; gli slot vuoti mostrano "Da definire".
• Promemoria domenicale — nuova notifica per pianificare la settimana prima di lunedì.
+1 -1
View File
@@ -1 +1 @@
NOVIDADE 1.0.6: Pratos prioritários, regras por dia, troca com toque e exporte a semana mesmo incompleta. NOVIDADE 1.1.0: Premium agora é uma assinatura mensal. Compradores anteriores mantêm o acesso completo automaticamente.
+3 -7
View File
@@ -1,8 +1,4 @@
Novidades na versão 1.0.6: Novidades na versão 1.1.0:
• Pratos prioritários — marque um prato com estrela para que a atribuição automática o escolha primeiro. • Premium agora é uma assinatura mensal — pratos ilimitados, planejamento e exportação por R$ 2,99/mês.
Regras de dias — restrições de etiquetas agora suportam "apenas dias úteis" ou "apenas fim de semana". Usuários com compra única anterior mantêm o acesso completo automaticamente.
• Botão auto-atribuir — agora visível diretamente quando há slots vazios.
• Toque para substituir — toque em um slot preenchido para trocar o prato diretamente.
• Exportar a qualquer hora — compartilhe sua semana mesmo incompleta; slots vazios mostram "A definir".
• Lembrete de domingo — nova notificação para planejar a semana antes da segunda-feira.