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:
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 +1,2 @@
|
|||||||
run_precheck_before_submit(false)
|
run_precheck_before_submit(false)
|
||||||
|
force(true)
|
||||||
|
|||||||
+2
-2
@@ -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 @@
|
|||||||
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.
|
||||||
|
|||||||
@@ -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 @@
|
|||||||
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.
|
||||||
|
|||||||
@@ -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 @@
|
|||||||
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.
|
||||||
|
|||||||
@@ -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 @@
|
|||||||
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.
|
||||||
|
|||||||
@@ -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 @@
|
|||||||
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.
|
||||||
|
|||||||
@@ -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 @@
|
|||||||
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.
|
||||||
|
|||||||
@@ -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.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user