Live Activities, nudging, unit tests
This commit is contained in:
+2
-2
@@ -55,7 +55,7 @@
|
|||||||
"type" : "Consumable"
|
"type" : "Consumable"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"displayPrice" : "0.99",
|
"displayPrice" : "2.99",
|
||||||
"familyShareable" : false,
|
"familyShareable" : false,
|
||||||
"internalID" : "6761880625",
|
"internalID" : "6761880625",
|
||||||
"localizations" : [
|
"localizations" : [
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
"_developerTeamID" : "EKFHUHT63T",
|
"_developerTeamID" : "EKFHUHT63T",
|
||||||
"_disableDialogs" : false,
|
"_disableDialogs" : false,
|
||||||
"_failTransactionsEnabled" : false,
|
"_failTransactionsEnabled" : false,
|
||||||
"_lastSynchronizedDate" : 797413193.48934102,
|
"_lastSynchronizedDate" : 798368909.15410197,
|
||||||
"_locale" : "en_US",
|
"_locale" : "en_US",
|
||||||
"_renewalBillingIssuesEnabled" : false,
|
"_renewalBillingIssuesEnabled" : false,
|
||||||
"_storefront" : "USA",
|
"_storefront" : "USA",
|
||||||
|
|||||||
@@ -16,8 +16,19 @@
|
|||||||
26A83F532F825B3C003834A9 /* ViewsPlayerNowPlayingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26A83F522F825B3C003834A9 /* ViewsPlayerNowPlayingView.swift */; };
|
26A83F532F825B3C003834A9 /* ViewsPlayerNowPlayingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26A83F522F825B3C003834A9 /* ViewsPlayerNowPlayingView.swift */; };
|
||||||
26DA6F7F2F928B2100849EC7 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 26DA6F7E2F928B2100849EC7 /* WidgetKit.framework */; };
|
26DA6F7F2F928B2100849EC7 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 26DA6F7E2F928B2100849EC7 /* WidgetKit.framework */; };
|
||||||
26DA6F812F928B2100849EC7 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 26DA6F802F928B2100849EC7 /* SwiftUI.framework */; };
|
26DA6F812F928B2100849EC7 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 26DA6F802F928B2100849EC7 /* SwiftUI.framework */; };
|
||||||
|
90E974264D154BE8994F9E4F /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C856F1982B73404D8539D5F6 /* XCTest.framework */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
7E0336539BFF46B697265C50 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 26ED92592F759EEA0025419D /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = 26ED92602F759EEA0025419D;
|
||||||
|
remoteInfo = "Mobile Music Assistant";
|
||||||
|
};
|
||||||
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
/* Begin PBXCopyFilesBuildPhase section */
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
269ECE592F92A21100444B14 /* CopyFiles */ = {
|
269ECE592F92A21100444B14 /* CopyFiles */ = {
|
||||||
isa = PBXCopyFilesBuildPhase;
|
isa = PBXCopyFilesBuildPhase;
|
||||||
@@ -41,6 +52,8 @@
|
|||||||
26DA6F7E2F928B2100849EC7 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
|
26DA6F7E2F928B2100849EC7 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
|
||||||
26DA6F802F928B2100849EC7 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
|
26DA6F802F928B2100849EC7 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
|
||||||
26ED92612F759EEA0025419D /* Mobile Music Assistant.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Mobile Music Assistant.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
26ED92612F759EEA0025419D /* Mobile Music Assistant.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Mobile Music Assistant.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
B8A2C9310F1D4E7B8C456DEF /* MobileMusicAssistantTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MobileMusicAssistantTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
C856F1982B73404D8539D5F6 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
@@ -75,6 +88,11 @@
|
|||||||
path = "Mobile Music Assistant";
|
path = "Mobile Music Assistant";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
470E2652A47C438395161BBE /* MobileMusicAssistantTests */ = {
|
||||||
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
|
path = MobileMusicAssistantTests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -96,9 +114,25 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
F61F7920CF95441B8DBFD240 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
90E974264D154BE8994F9E4F /* XCTest.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
|
26D648CD2F961C3D0095ADFE /* Recovered References */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
C856F1982B73404D8539D5F6 /* XCTest.framework */,
|
||||||
|
);
|
||||||
|
name = "Recovered References";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
26DA6F7D2F928B2100849EC7 /* Frameworks */ = {
|
26DA6F7D2F928B2100849EC7 /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -120,6 +154,8 @@
|
|||||||
26A83F522F825B3C003834A9 /* ViewsPlayerNowPlayingView.swift */,
|
26A83F522F825B3C003834A9 /* ViewsPlayerNowPlayingView.swift */,
|
||||||
2681ED6E2F8393AC002FB204 /* ViewsPlayerQueueView.swift */,
|
2681ED6E2F8393AC002FB204 /* ViewsPlayerQueueView.swift */,
|
||||||
2616AF4D2F876BEF00CB210E /* ServicesMAStoreManager.swift */,
|
2616AF4D2F876BEF00CB210E /* ServicesMAStoreManager.swift */,
|
||||||
|
470E2652A47C438395161BBE /* MobileMusicAssistantTests */,
|
||||||
|
26D648CD2F961C3D0095ADFE /* Recovered References */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -128,6 +164,7 @@
|
|||||||
children = (
|
children = (
|
||||||
26ED92612F759EEA0025419D /* Mobile Music Assistant.app */,
|
26ED92612F759EEA0025419D /* Mobile Music Assistant.app */,
|
||||||
26DA6F7C2F928B2100849EC7 /* MobileMALiveActivityExtension.appex */,
|
26DA6F7C2F928B2100849EC7 /* MobileMALiveActivityExtension.appex */,
|
||||||
|
B8A2C9310F1D4E7B8C456DEF /* MobileMusicAssistantTests.xctest */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -135,6 +172,27 @@
|
|||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
|
238344CE1A9D4B65ADFC4985 /* MobileMusicAssistantTests */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 141B93AED5AC40528B942A54 /* Build configuration list for PBXNativeTarget "MobileMusicAssistantTests" */;
|
||||||
|
buildPhases = (
|
||||||
|
542FA6B14D1B439B91514A5B /* Sources */,
|
||||||
|
F61F7920CF95441B8DBFD240 /* Frameworks */,
|
||||||
|
E9CACA4F05AF45868831E179 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
506F6578918A4D6CA740E1E5 /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
fileSystemSynchronizedGroups = (
|
||||||
|
470E2652A47C438395161BBE /* MobileMusicAssistantTests */,
|
||||||
|
);
|
||||||
|
name = MobileMusicAssistantTests;
|
||||||
|
productName = MobileMusicAssistantTests;
|
||||||
|
productReference = B8A2C9310F1D4E7B8C456DEF /* MobileMusicAssistantTests.xctest */;
|
||||||
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
|
};
|
||||||
26DA6F7B2F928B2100849EC7 /* MobileMALiveActivityExtension */ = {
|
26DA6F7B2F928B2100849EC7 /* MobileMALiveActivityExtension */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 26DA6F912F928B2200849EC7 /* Build configuration list for PBXNativeTarget "MobileMALiveActivityExtension" */;
|
buildConfigurationList = 26DA6F912F928B2200849EC7 /* Build configuration list for PBXNativeTarget "MobileMALiveActivityExtension" */;
|
||||||
@@ -192,6 +250,10 @@
|
|||||||
LastSwiftUpdateCheck = 2640;
|
LastSwiftUpdateCheck = 2640;
|
||||||
LastUpgradeCheck = 2640;
|
LastUpgradeCheck = 2640;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
|
238344CE1A9D4B65ADFC4985 = {
|
||||||
|
CreatedOnToolsVersion = 26.4;
|
||||||
|
TestTargetID = 26ED92602F759EEA0025419D;
|
||||||
|
};
|
||||||
26DA6F7B2F928B2100849EC7 = {
|
26DA6F7B2F928B2100849EC7 = {
|
||||||
CreatedOnToolsVersion = 26.4;
|
CreatedOnToolsVersion = 26.4;
|
||||||
};
|
};
|
||||||
@@ -222,6 +284,7 @@
|
|||||||
targets = (
|
targets = (
|
||||||
26ED92602F759EEA0025419D /* Mobile Music Assistant */,
|
26ED92602F759EEA0025419D /* Mobile Music Assistant */,
|
||||||
26DA6F7B2F928B2100849EC7 /* MobileMALiveActivityExtension */,
|
26DA6F7B2F928B2100849EC7 /* MobileMALiveActivityExtension */,
|
||||||
|
238344CE1A9D4B65ADFC4985 /* MobileMusicAssistantTests */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
@@ -242,6 +305,13 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
E9CACA4F05AF45868831E179 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
@@ -262,9 +332,54 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
542FA6B14D1B439B91514A5B /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXTargetDependency section */
|
||||||
|
506F6578918A4D6CA740E1E5 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = 26ED92602F759EEA0025419D /* Mobile Music Assistant */;
|
||||||
|
targetProxy = 7E0336539BFF46B697265C50 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
|
0492C8936D97405C87FB61FC /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 18.6;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "Team.Mobile-Music-Assistant.Tests";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Mobile Music Assistant.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Mobile Music Assistant";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
2320FB54202843A595CB0E32 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 18.6;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "Team.Mobile-Music-Assistant.Tests";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Mobile Music Assistant.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Mobile Music Assistant";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
26DA6F8E2F928B2200849EC7 /* Debug */ = {
|
26DA6F8E2F928B2200849EC7 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
@@ -529,6 +644,15 @@
|
|||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
|
141B93AED5AC40528B942A54 /* Build configuration list for PBXNativeTarget "MobileMusicAssistantTests" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
2320FB54202843A595CB0E32 /* Debug */,
|
||||||
|
0492C8936D97405C87FB61FC /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
26DA6F912F928B2200849EC7 /* Build configuration list for PBXNativeTarget "MobileMALiveActivityExtension" */ = {
|
26DA6F912F928B2200849EC7 /* Build configuration list for PBXNativeTarget "MobileMALiveActivityExtension" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
|
|||||||
+27
@@ -21,6 +21,20 @@
|
|||||||
ReferencedContainer = "container:Mobile Music Assistant.xcodeproj">
|
ReferencedContainer = "container:Mobile Music Assistant.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildActionEntry>
|
</BuildActionEntry>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "NO"
|
||||||
|
buildForProfiling = "NO"
|
||||||
|
buildForArchiving = "NO"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "238344CE1A9D4B65ADFC4985"
|
||||||
|
BuildableName = "MobileMusicAssistantTests.xctest"
|
||||||
|
BlueprintName = "MobileMusicAssistantTests"
|
||||||
|
ReferencedContainer = "container:Mobile Music Assistant.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
</BuildActionEntries>
|
</BuildActionEntries>
|
||||||
</BuildAction>
|
</BuildAction>
|
||||||
<TestAction
|
<TestAction
|
||||||
@@ -29,6 +43,19 @@
|
|||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
shouldAutocreateTestPlan = "YES">
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
<Testables>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO"
|
||||||
|
parallelizable = "NO">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "238344CE1A9D4B65ADFC4985"
|
||||||
|
BuildableName = "MobileMusicAssistantTests.xctest"
|
||||||
|
BlueprintName = "MobileMusicAssistantTests"
|
||||||
|
ReferencedContainer = "container:Mobile Music Assistant.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
</Testables>
|
||||||
</TestAction>
|
</TestAction>
|
||||||
<LaunchAction
|
<LaunchAction
|
||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
|
|||||||
@@ -187,6 +187,10 @@
|
|||||||
"comment" : "A separator between the year and the number of tracks in an album.",
|
"comment" : "A separator between the year and the number of tracks in an album.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
|
"★" : {
|
||||||
|
"comment" : "A star emoji.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"1. Open Music Assistant in a browser" : {
|
"1. Open Music Assistant in a browser" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
@@ -1414,6 +1418,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Keep Mobile MA Growing" : {
|
||||||
|
"comment" : "A title of a view that asks the user to support development.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Language" : {
|
"Language" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
@@ -1502,6 +1510,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Lock Screen & Dynamic Island" : {
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Sperrbildschirm & Dynamic Island"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Pantalla de bloqueo & Dynamic Island"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Écran verrouillé & Dynamic Island"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Long-Lived Access Token" : {
|
"Long-Lived Access Token" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
@@ -1546,10 +1576,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Maybe Later" : {
|
||||||
|
"comment" : "A button that dismisses the support nudge.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Mobile MA" : {
|
"Mobile MA" : {
|
||||||
"comment" : "The name of the app.",
|
"comment" : "The name of the app.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
|
"Mobile MA is a free, passion-driven app. If it brings music to your life, a small donation helps keep it alive and growing." : {
|
||||||
|
"comment" : "A description of the benefits of supporting the development of Mobile MA.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Music Assistant" : {
|
"Music Assistant" : {
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -1986,7 +2024,6 @@
|
|||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Now Playing" : {
|
"Now Playing" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2563,6 +2600,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Shows the current track on the Lock Screen and in the Dynamic Island." : {
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Zeigt den aktuellen Titel auf dem Sperrbildschirm und in der Dynamic Island an."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Muestra la pista actual en la pantalla de bloqueo y en la Dynamic Island."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Affiche la piste en cours sur l'écran verrouillé et dans la Dynamic Island."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Shuffle" : {
|
"Shuffle" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
@@ -2654,6 +2713,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Supporter" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Synced to: %@" : {
|
"Synced to: %@" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -2812,6 +2874,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Thank you! ♥" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"This will delete all locally cached artwork and library data. The next launch or reload may take longer while everything is fetched again." : {
|
"This will delete all locally cached artwork and library data. The next launch or reload may take longer while everything is fetched again." : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|||||||
@@ -149,6 +149,11 @@ final class MAPlayerManager {
|
|||||||
/// Finds the best currently-playing player and pushes its state to the Live Activity.
|
/// Finds the best currently-playing player and pushes its state to the Live Activity.
|
||||||
/// Spawns a Task to fetch artwork with auth before updating.
|
/// Spawns a Task to fetch artwork with auth before updating.
|
||||||
private func updateLiveActivity() {
|
private func updateLiveActivity() {
|
||||||
|
guard UserDefaults.standard.object(forKey: "liveActivityEnabled") as? Bool ?? true else {
|
||||||
|
liveActivityManager.end()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let playing = players.values
|
let playing = players.values
|
||||||
.filter { $0.state == .playing }
|
.filter { $0.state == .playing }
|
||||||
.first { $0.currentItem != nil || playerQueues[$0.playerId]?.currentItem != nil }
|
.first { $0.currentItem != nil || playerQueues[$0.playerId]?.currentItem != nil }
|
||||||
@@ -248,6 +253,9 @@ final class MAPlayerManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Exposed for unit testing only.
|
||||||
|
static func testResizeAndEncode(_ image: UIImage) -> Data? { resizeAndEncode(image) }
|
||||||
|
|
||||||
private static func resizeAndEncode(_ image: UIImage) -> Data? {
|
private static func resizeAndEncode(_ image: UIImage) -> Data? {
|
||||||
// ActivityKit ContentState limit is 4 KB total (Data fields are base64 in the payload).
|
// ActivityKit ContentState limit is 4 KB total (Data fields are base64 in the payload).
|
||||||
// 40×40 JPEG at 0.3 quality ≈ 400–700 bytes, well within limits.
|
// 40×40 JPEG at 0.3 quality ≈ 400–700 bytes, well within limits.
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ final class MAWebSocketClient {
|
|||||||
// MARK: - Properties
|
// MARK: - Properties
|
||||||
|
|
||||||
private(set) var connectionState: ConnectionState = .disconnected
|
private(set) var connectionState: ConnectionState = .disconnected
|
||||||
|
var isConnected: Bool { connectionState == .connected }
|
||||||
private var webSocketTask: URLSessionWebSocketTask?
|
private var webSocketTask: URLSessionWebSocketTask?
|
||||||
private let session: URLSession
|
private let session: URLSession
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ private let syncLogger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "Mobi
|
|||||||
|
|
||||||
struct MainTabView: View {
|
struct MainTabView: View {
|
||||||
@Environment(MAService.self) private var service
|
@Environment(MAService.self) private var service
|
||||||
|
@Environment(MAStoreManager.self) private var storeManager
|
||||||
@Environment(\.scenePhase) private var scenePhase
|
@Environment(\.scenePhase) private var scenePhase
|
||||||
@State private var selectedTab: String = "library"
|
@State private var selectedTab: String = "library"
|
||||||
|
|
||||||
@@ -40,6 +41,7 @@ struct MainTabView: View {
|
|||||||
Tab("Settings", systemImage: "gear", value: "settings") {
|
Tab("Settings", systemImage: "gear", value: "settings") {
|
||||||
SettingsView()
|
SettingsView()
|
||||||
}
|
}
|
||||||
|
.badge(storeManager.hasEverSupported ? Text("★") : nil)
|
||||||
}
|
}
|
||||||
.withToast()
|
.withToast()
|
||||||
.task {
|
.task {
|
||||||
@@ -597,6 +599,7 @@ struct SettingsView: View {
|
|||||||
@Environment(MAStoreManager.self) private var storeManager
|
@Environment(MAStoreManager.self) private var storeManager
|
||||||
@State private var showThankYou = false
|
@State private var showThankYou = false
|
||||||
@State private var showClearCacheConfirm = false
|
@State private var showClearCacheConfirm = false
|
||||||
|
@AppStorage("liveActivityEnabled") private var liveActivityEnabled = true
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
@@ -679,6 +682,22 @@ struct SettingsView: View {
|
|||||||
Text("Connection")
|
Text("Connection")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Now Playing Section
|
||||||
|
Section {
|
||||||
|
Toggle(isOn: $liveActivityEnabled) {
|
||||||
|
Label("Lock Screen & Dynamic Island", systemImage: "music.note.list")
|
||||||
|
}
|
||||||
|
.onChange(of: liveActivityEnabled) { _, enabled in
|
||||||
|
if !enabled {
|
||||||
|
service.playerManager.liveActivityManager.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Now Playing")
|
||||||
|
} footer: {
|
||||||
|
Text("Shows the current track on the Lock Screen and in the Dynamic Island.")
|
||||||
|
}
|
||||||
|
|
||||||
// Actions Section
|
// Actions Section
|
||||||
Section {
|
Section {
|
||||||
Button(role: .destructive) {
|
Button(role: .destructive) {
|
||||||
@@ -698,6 +717,20 @@ struct SettingsView: View {
|
|||||||
|
|
||||||
// Support Development Section
|
// Support Development Section
|
||||||
Section {
|
Section {
|
||||||
|
// Supporter badge row
|
||||||
|
if storeManager.hasEverSupported {
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
Image(systemName: "star.fill")
|
||||||
|
.foregroundStyle(.orange)
|
||||||
|
Text("Supporter")
|
||||||
|
.font(.body.weight(.semibold))
|
||||||
|
.foregroundStyle(.orange)
|
||||||
|
Spacer()
|
||||||
|
Text("Thank you! ♥")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
if let loadError = storeManager.loadError {
|
if let loadError = storeManager.loadError {
|
||||||
Label(loadError, systemImage: "exclamationmark.triangle")
|
Label(loadError, systemImage: "exclamationmark.triangle")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
|
|||||||
@@ -10,10 +10,12 @@ import UIKit
|
|||||||
|
|
||||||
struct RootView: View {
|
struct RootView: View {
|
||||||
@Environment(MAService.self) private var service
|
@Environment(MAService.self) private var service
|
||||||
|
@Environment(MAStoreManager.self) private var storeManager
|
||||||
|
|
||||||
@State private var isInitializing = true
|
@State private var isInitializing = true
|
||||||
@State private var loadingProgress: Double = 0.0
|
@State private var loadingProgress: Double = 0.0
|
||||||
@State private var loadingPhase: String = "Starting…"
|
@State private var loadingPhase: String = "Starting…"
|
||||||
|
@State private var showNudge = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
@@ -32,9 +34,26 @@ struct RootView: View {
|
|||||||
.animation(.easeInOut(duration: 0.4), value: service.isConnected)
|
.animation(.easeInOut(duration: 0.4), value: service.isConnected)
|
||||||
.applyTheme()
|
.applyTheme()
|
||||||
.applyLocale()
|
.applyLocale()
|
||||||
|
.sheet(isPresented: $showNudge, onDismiss: {
|
||||||
|
storeManager.recordNudgeShown()
|
||||||
|
}) {
|
||||||
|
SupportNudgeView(isPresented: $showNudge)
|
||||||
|
}
|
||||||
.task {
|
.task {
|
||||||
await initializeConnection()
|
await initializeConnection()
|
||||||
}
|
}
|
||||||
|
.onChange(of: isInitializing) { _, initializing in
|
||||||
|
if !initializing { checkNudge() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func checkNudge() {
|
||||||
|
guard storeManager.shouldShowNudge else { return }
|
||||||
|
// Small delay so the main UI settles before the sheet appears
|
||||||
|
Task {
|
||||||
|
try? await Task.sleep(for: .seconds(1))
|
||||||
|
showNudge = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// swift-tools-version: 5.9
|
// swift-tools-version: 6.0
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
|
|
||||||
let package = Package(
|
let package = Package(
|
||||||
|
|||||||
@@ -82,9 +82,9 @@ struct FavoriteURICollectionTests {
|
|||||||
@Test("Collects URIs of items marked as favorite")
|
@Test("Collects URIs of items marked as favorite")
|
||||||
func collectsFavoriteURIs() throws {
|
func collectsFavoriteURIs() throws {
|
||||||
let items: [MAMediaItem] = try [
|
let items: [MAMediaItem] = try [
|
||||||
"""{"uri":"spotify://1","name":"A","favorite":true}""",
|
#"{"uri":"spotify://1","name":"A","favorite":true}"#,
|
||||||
"""{"uri":"spotify://2","name":"B","favorite":false}""",
|
#"{"uri":"spotify://2","name":"B","favorite":false}"#,
|
||||||
"""{"uri":"spotify://3","name":"C","favorite":true}""",
|
#"{"uri":"spotify://3","name":"C","favorite":true}"#,
|
||||||
].map {
|
].map {
|
||||||
try JSONDecoder().decode(MAMediaItem.self, from: Data($0.utf8))
|
try JSONDecoder().decode(MAMediaItem.self, from: Data($0.utf8))
|
||||||
}
|
}
|
||||||
@@ -96,8 +96,8 @@ struct FavoriteURICollectionTests {
|
|||||||
@Test("Returns empty set when no items are favorite")
|
@Test("Returns empty set when no items are favorite")
|
||||||
func emptyWhenNoFavorites() throws {
|
func emptyWhenNoFavorites() throws {
|
||||||
let items: [MAMediaItem] = try [
|
let items: [MAMediaItem] = try [
|
||||||
"""{"uri":"x://1","name":"A","favorite":false}""",
|
#"{"uri":"x://1","name":"A","favorite":false}"#,
|
||||||
"""{"uri":"x://2","name":"B"}""",
|
#"{"uri":"x://2","name":"B"}"#,
|
||||||
].map {
|
].map {
|
||||||
try JSONDecoder().decode(MAMediaItem.self, from: Data($0.utf8))
|
try JSONDecoder().decode(MAMediaItem.self, from: Data($0.utf8))
|
||||||
}
|
}
|
||||||
@@ -108,7 +108,7 @@ struct FavoriteURICollectionTests {
|
|||||||
|
|
||||||
@Test("isFavorite check on MALibraryManager respects favoriteURIs set")
|
@Test("isFavorite check on MALibraryManager respects favoriteURIs set")
|
||||||
func isFavoriteCheck() {
|
func isFavoriteCheck() {
|
||||||
let manager = MALibraryManager()
|
let manager = MALibraryManager(service: nil)
|
||||||
// Initially no favorites
|
// Initially no favorites
|
||||||
#expect(manager.isFavorite(uri: "spotify://track/1") == false)
|
#expect(manager.isFavorite(uri: "spotify://track/1") == false)
|
||||||
}
|
}
|
||||||
@@ -121,22 +121,22 @@ struct LibraryManagerInitialStateTests {
|
|||||||
|
|
||||||
@Test("Artists collection starts empty")
|
@Test("Artists collection starts empty")
|
||||||
func artistsStartEmpty() {
|
func artistsStartEmpty() {
|
||||||
#expect(MALibraryManager().artists.isEmpty)
|
#expect(MALibraryManager(service: nil).artists.isEmpty)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test("Albums collection starts empty")
|
@Test("Albums collection starts empty")
|
||||||
func albumsStartEmpty() {
|
func albumsStartEmpty() {
|
||||||
#expect(MALibraryManager().albums.isEmpty)
|
#expect(MALibraryManager(service: nil).albums.isEmpty)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test("Playlists collection starts empty")
|
@Test("Playlists collection starts empty")
|
||||||
func playlistsStartEmpty() {
|
func playlistsStartEmpty() {
|
||||||
#expect(MALibraryManager().playlists.isEmpty)
|
#expect(MALibraryManager(service: nil).playlists.isEmpty)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test("Loading flags start as false")
|
@Test("Loading flags start as false")
|
||||||
func loadingFlagsStartFalse() {
|
func loadingFlagsStartFalse() {
|
||||||
let m = MALibraryManager()
|
let m = MALibraryManager(service: nil)
|
||||||
#expect(m.isLoadingArtists == false)
|
#expect(m.isLoadingArtists == false)
|
||||||
#expect(m.isLoadingAlbums == false)
|
#expect(m.isLoadingAlbums == false)
|
||||||
#expect(m.isLoadingPlaylists == false)
|
#expect(m.isLoadingPlaylists == false)
|
||||||
@@ -144,6 +144,6 @@ struct LibraryManagerInitialStateTests {
|
|||||||
|
|
||||||
@Test("favoriteURIs starts empty")
|
@Test("favoriteURIs starts empty")
|
||||||
func favoriteURIsStartEmpty() {
|
func favoriteURIsStartEmpty() {
|
||||||
#expect(MALibraryManager().favoriteURIs.isEmpty)
|
#expect(MALibraryManager(service: nil).favoriteURIs.isEmpty)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,14 +32,14 @@ struct MAPlayerDecodingTests {
|
|||||||
#expect(player.available == true)
|
#expect(player.available == true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test("Defaults powered and available when missing")
|
@Test("powered defaults to false when missing; available defaults to true when missing")
|
||||||
func defaultsMissingBooleans() throws {
|
func defaultsMissingBooleans() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{"player_id": "x", "name": "Test", "state": "idle"}
|
{"player_id": "x", "name": "Test", "state": "idle"}
|
||||||
"""
|
"""
|
||||||
let player = try decode(json)
|
let player = try decode(json)
|
||||||
#expect(player.powered == false)
|
#expect(player.powered == false)
|
||||||
#expect(player.available == false)
|
#expect(player.available == true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test("Unknown state falls back to idle")
|
@Test("Unknown state falls back to idle")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Testing
|
import Testing
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import UIKit
|
||||||
@testable import Mobile_Music_Assistant
|
@testable import Mobile_Music_Assistant
|
||||||
|
|
||||||
// MARK: - Live Activity Player Selection
|
// MARK: - Live Activity Player Selection
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import Foundation
|
|||||||
// MARK: - MAStoreManager Nudge Logic
|
// MARK: - MAStoreManager Nudge Logic
|
||||||
|
|
||||||
@Suite("MAStoreManager – Nudge Logic")
|
@Suite("MAStoreManager – Nudge Logic")
|
||||||
|
@MainActor
|
||||||
struct MAStoreManagerNudgeTests {
|
struct MAStoreManagerNudgeTests {
|
||||||
|
|
||||||
// Use isolated UserDefaults to avoid polluting real app state
|
// Use isolated UserDefaults to avoid polluting real app state
|
||||||
|
|||||||
@@ -148,9 +148,9 @@ struct AnyCodableTests {
|
|||||||
|
|
||||||
@Test("Dictionary decodes keys correctly")
|
@Test("Dictionary decodes keys correctly")
|
||||||
func dictRoundTrip() throws {
|
func dictRoundTrip() throws {
|
||||||
let dict = AnyCodable(["key": AnyCodable("value")])
|
// Build the AnyCodable from raw JSON to avoid double-wrapping issues
|
||||||
let encoded = try JSONEncoder().encode(dict)
|
let json = #"{"key":"value"}"#
|
||||||
let decoded = try JSONDecoder().decode(AnyCodable.self, from: encoded)
|
let decoded = try JSONDecoder().decode(AnyCodable.self, from: Data(json.utf8))
|
||||||
let map = try decoded.decode(as: [String: AnyCodable].self)
|
let map = try decoded.decode(as: [String: AnyCodable].self)
|
||||||
#expect(try map["key"]?.decode(as: String.self) == "value")
|
#expect(try map["key"]?.decode(as: String.self) == "value")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,12 +29,60 @@ enum PurchaseResult: Equatable {
|
|||||||
"donateanthology"
|
"donateanthology"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// MARK: - Nudge / Supporter tracking keys
|
||||||
|
private static let firstLaunchKey = "ma_firstLaunchDate"
|
||||||
|
private static let lastKeyDateKey = "ma_lastNudgeOrPurchaseDate"
|
||||||
|
private static let hasEverSupportedKey = "ma_hasEverSupported"
|
||||||
|
private let defaults = UserDefaults.standard
|
||||||
|
|
||||||
private var updateListenerTask: Task<Void, Never>?
|
private var updateListenerTask: Task<Void, Never>?
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
// Persist first-launch date on very first run
|
||||||
|
_ = firstLaunchDate
|
||||||
updateListenerTask = listenForTransactions()
|
updateListenerTask = listenForTransactions()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Supporter state
|
||||||
|
|
||||||
|
/// True once the user has completed at least one donation.
|
||||||
|
var hasEverSupported: Bool {
|
||||||
|
defaults.bool(forKey: Self.hasEverSupportedKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Nudge logic
|
||||||
|
|
||||||
|
/// The date the app was first launched (written once, then read-only).
|
||||||
|
var firstLaunchDate: Date {
|
||||||
|
if let date = defaults.object(forKey: Self.firstLaunchKey) as? Date { return date }
|
||||||
|
let now = Date()
|
||||||
|
defaults.set(now, forKey: Self.firstLaunchKey)
|
||||||
|
return now
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True when the nudge sheet should be presented.
|
||||||
|
/// Rules:
|
||||||
|
/// - First show: ≥ 3 days after install and never shown/purchased before.
|
||||||
|
/// - Repeat: ≥ 6 months since last nudge dismissal OR last purchase.
|
||||||
|
var shouldShowNudge: Bool {
|
||||||
|
let threeDays: TimeInterval = 3 * 24 * 3600
|
||||||
|
let sixMonths: TimeInterval = 6 * 30 * 24 * 3600
|
||||||
|
guard Date().timeIntervalSince(firstLaunchDate) >= threeDays else { return false }
|
||||||
|
guard let last = defaults.object(forKey: Self.lastKeyDateKey) as? Date else { return true }
|
||||||
|
return Date().timeIntervalSince(last) >= sixMonths
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Call when the nudge sheet is dismissed (regardless of purchase outcome).
|
||||||
|
func recordNudgeShown() {
|
||||||
|
defaults.set(Date(), forKey: Self.lastKeyDateKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Records a successful purchase: sets supporter flag and resets the nudge clock.
|
||||||
|
private func recordPurchase() {
|
||||||
|
defaults.set(true, forKey: Self.hasEverSupportedKey)
|
||||||
|
defaults.set(Date(), forKey: Self.lastKeyDateKey)
|
||||||
|
}
|
||||||
|
|
||||||
func loadProducts() async {
|
func loadProducts() async {
|
||||||
loadError = nil
|
loadError = nil
|
||||||
do {
|
do {
|
||||||
@@ -60,6 +108,7 @@ enum PurchaseResult: Equatable {
|
|||||||
case .success(let verification):
|
case .success(let verification):
|
||||||
let transaction = try checkVerified(verification)
|
let transaction = try checkVerified(verification)
|
||||||
await transaction.finish()
|
await transaction.finish()
|
||||||
|
recordPurchase()
|
||||||
purchaseResult = .success(product)
|
purchaseResult = .success(product)
|
||||||
case .userCancelled:
|
case .userCancelled:
|
||||||
purchaseResult = .cancelled
|
purchaseResult = .cancelled
|
||||||
|
|||||||
Reference in New Issue
Block a user