mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-25 09:41:02 +00:00
Add push notifications support
This commit is contained in:
parent
a1a6c3091e
commit
424cd475ad
16 changed files with 768 additions and 16 deletions
|
@ -13,6 +13,12 @@
|
|||
9F2A540A29699705009B2D7C /* ReceiptParser in Frameworks */ = {isa = PBXBuildFile; productRef = 9F2A540929699705009B2D7C /* ReceiptParser */; };
|
||||
9F2A540C29699705009B2D7C /* RevenueCat in Frameworks */ = {isa = PBXBuildFile; productRef = 9F2A540B29699705009B2D7C /* RevenueCat */; };
|
||||
9F2A540E2969A0B0009B2D7C /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F2A540D2969A0B0009B2D7C /* StoreKit.framework */; };
|
||||
9F2A5411296A1429009B2D7C /* PushNotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2A5410296A1429009B2D7C /* PushNotificationsView.swift */; };
|
||||
9F2A5419296AB631009B2D7C /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2A5418296AB631009B2D7C /* NotificationService.swift */; };
|
||||
9F2A541D296AB631009B2D7C /* IceCubesNotifications.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 9F2A5416296AB631009B2D7C /* IceCubesNotifications.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
9F2A5424296AB67A009B2D7C /* Env in Frameworks */ = {isa = PBXBuildFile; productRef = 9F2A5423296AB67A009B2D7C /* Env */; };
|
||||
9F2A5426296AB67E009B2D7C /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 9F2A5425296AB67E009B2D7C /* KeychainSwift */; };
|
||||
9F2A5428296AB683009B2D7C /* Models in Frameworks */ = {isa = PBXBuildFile; productRef = 9F2A5427296AB683009B2D7C /* Models */; };
|
||||
9F2B92F6295AE04800DE16D0 /* Tabs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2B92F5295AE04800DE16D0 /* Tabs.swift */; };
|
||||
9F2B92FA295DA7D700DE16D0 /* AddAccountsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2B92F9295DA7D700DE16D0 /* AddAccountsView.swift */; };
|
||||
9F2B92FC295DA94500DE16D0 /* InstanceInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2B92FB295DA94500DE16D0 /* InstanceInfoView.swift */; };
|
||||
|
@ -46,6 +52,30 @@
|
|||
9FE151A6293C90F900E9683D /* IconSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE151A5293C90F900E9683D /* IconSelectorView.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
9F2A541B296AB631009B2D7C /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 9FBFE631292A715500C250E9 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 9F2A5415296AB631009B2D7C;
|
||||
remoteInfo = IceCubesNotifications;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
9F2A5421296AB631009B2D7C /* Embed Foundation Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
9F2A541D296AB631009B2D7C /* IceCubesNotifications.appex in Embed Foundation Extensions */,
|
||||
);
|
||||
name = "Embed Foundation Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
9F24EEB729360C330042359D /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||
9F29553D292B67B600E0E81B /* Network */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Network; path = Packages/Network; sourceTree = "<group>"; };
|
||||
|
@ -53,6 +83,11 @@
|
|||
9F2A5404296995FB009B2D7C /* QuickLookUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLookUI.framework; path = System/Library/Frameworks/QuickLookUI.framework; sourceTree = SDKROOT; };
|
||||
9F2A540629699698009B2D7C /* SupportAppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportAppView.swift; sourceTree = "<group>"; };
|
||||
9F2A540D2969A0B0009B2D7C /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
|
||||
9F2A5410296A1429009B2D7C /* PushNotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationsView.swift; sourceTree = "<group>"; };
|
||||
9F2A5416296AB631009B2D7C /* IceCubesNotifications.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = IceCubesNotifications.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
9F2A5418296AB631009B2D7C /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
|
||||
9F2A541A296AB631009B2D7C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
9F2A5422296AB64B009B2D7C /* IceCubesNotifications.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = IceCubesNotifications.entitlements; sourceTree = "<group>"; };
|
||||
9F2B92F5295AE04800DE16D0 /* Tabs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tabs.swift; sourceTree = "<group>"; };
|
||||
9F2B92F9295DA7D700DE16D0 /* AddAccountsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccountsView.swift; sourceTree = "<group>"; };
|
||||
9F2B92FB295DA94500DE16D0 /* InstanceInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceInfoView.swift; sourceTree = "<group>"; };
|
||||
|
@ -89,6 +124,16 @@
|
|||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
9F2A5413296AB631009B2D7C /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9F2A5428296AB683009B2D7C /* Models in Frameworks */,
|
||||
9F2A5426296AB67E009B2D7C /* KeychainSwift in Frameworks */,
|
||||
9F2A5424296AB67A009B2D7C /* Env in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
9FBFE636292A715500C250E9 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
@ -115,6 +160,16 @@
|
|||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
9F2A5417296AB631009B2D7C /* IceCubesNotifications */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9F2A5422296AB64B009B2D7C /* IceCubesNotifications.entitlements */,
|
||||
9F2A5418296AB631009B2D7C /* NotificationService.swift */,
|
||||
9F2A541A296AB631009B2D7C /* Info.plist */,
|
||||
);
|
||||
path = IceCubesNotifications;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9F398AB429360A5800A889F2 /* App */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -172,6 +227,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
9FBFE63B292A715500C250E9 /* IceCubesApp */,
|
||||
9F2A5417296AB631009B2D7C /* IceCubesNotifications */,
|
||||
9FBFE63A292A715500C250E9 /* Products */,
|
||||
9FBFE64C292A72BD00C250E9 /* Frameworks */,
|
||||
9F398AAC2936005300A889F2 /* Account */,
|
||||
|
@ -192,6 +248,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
9FBFE639292A715500C250E9 /* IceCubesApp.app */,
|
||||
9F2A5416296AB631009B2D7C /* IceCubesNotifications.appex */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
|
@ -228,6 +285,7 @@
|
|||
9F2B92FB295DA94500DE16D0 /* InstanceInfoView.swift */,
|
||||
9F7335F82968576500AFF0BA /* DisplaySettingsView.swift */,
|
||||
9F2A540629699698009B2D7C /* SupportAppView.swift */,
|
||||
9F2A5410296A1429009B2D7C /* PushNotificationsView.swift */,
|
||||
);
|
||||
path = Settings;
|
||||
sourceTree = "<group>";
|
||||
|
@ -235,6 +293,28 @@
|
|||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
9F2A5415296AB631009B2D7C /* IceCubesNotifications */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 9F2A541E296AB631009B2D7C /* Build configuration list for PBXNativeTarget "IceCubesNotifications" */;
|
||||
buildPhases = (
|
||||
9F2A5412296AB631009B2D7C /* Sources */,
|
||||
9F2A5413296AB631009B2D7C /* Frameworks */,
|
||||
9F2A5414296AB631009B2D7C /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = IceCubesNotifications;
|
||||
packageProductDependencies = (
|
||||
9F2A5423296AB67A009B2D7C /* Env */,
|
||||
9F2A5425296AB67E009B2D7C /* KeychainSwift */,
|
||||
9F2A5427296AB683009B2D7C /* Models */,
|
||||
);
|
||||
productName = IceCubesNotifications;
|
||||
productReference = 9F2A5416296AB631009B2D7C /* IceCubesNotifications.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
9FBFE638292A715500C250E9 /* IceCubesApp */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 9FBFE648292A715600C250E9 /* Build configuration list for PBXNativeTarget "IceCubesApp" */;
|
||||
|
@ -242,10 +322,12 @@
|
|||
9FBFE635292A715500C250E9 /* Sources */,
|
||||
9FBFE636292A715500C250E9 /* Frameworks */,
|
||||
9FBFE637292A715500C250E9 /* Resources */,
|
||||
9F2A5421296AB631009B2D7C /* Embed Foundation Extensions */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
9F2A541C296AB631009B2D7C /* PBXTargetDependency */,
|
||||
);
|
||||
name = IceCubesApp;
|
||||
packageProductDependencies = (
|
||||
|
@ -274,9 +356,12 @@
|
|||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 1410;
|
||||
LastSwiftUpdateCheck = 1420;
|
||||
LastUpgradeCheck = 1420;
|
||||
TargetAttributes = {
|
||||
9F2A5415296AB631009B2D7C = {
|
||||
CreatedOnToolsVersion = 14.2;
|
||||
};
|
||||
9FBFE638292A715500C250E9 = {
|
||||
CreatedOnToolsVersion = 14.1;
|
||||
};
|
||||
|
@ -300,11 +385,19 @@
|
|||
projectRoot = "";
|
||||
targets = (
|
||||
9FBFE638292A715500C250E9 /* IceCubesApp */,
|
||||
9F2A5415296AB631009B2D7C /* IceCubesNotifications */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
9F2A5414296AB631009B2D7C /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
9FBFE637292A715500C250E9 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
@ -317,6 +410,14 @@
|
|||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
9F2A5412296AB631009B2D7C /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9F2A5419296AB631009B2D7C /* NotificationService.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
9FBFE635292A715500C250E9 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
@ -340,12 +441,82 @@
|
|||
9F7335F72968274500AFF0BA /* AppAccountsSelectorView.swift in Sources */,
|
||||
9FAE4AD129379AD600772766 /* AppAccount.swift in Sources */,
|
||||
9F55C68D2955968700F94077 /* ExploreTab.swift in Sources */,
|
||||
9F2A5411296A1429009B2D7C /* PushNotificationsView.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
9F2A541C296AB631009B2D7C /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 9F2A5415296AB631009B2D7C /* IceCubesNotifications */;
|
||||
targetProxy = 9F2A541B296AB631009B2D7C /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
9F2A541F296AB631009B2D7C /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = IceCubesNotifications/IceCubesNotifications.entitlements;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 730;
|
||||
DEVELOPMENT_TEAM = Z6P74P6T99;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = IceCubesNotifications/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = IceCubesNotifications;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.7;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.thomasricouard.IceCubesApp.IceCubesNotifications;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
9F2A5420296AB631009B2D7C /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = IceCubesNotifications/IceCubesNotifications.entitlements;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 730;
|
||||
DEVELOPMENT_TEAM = Z6P74P6T99;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = IceCubesNotifications/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = IceCubesNotifications;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.7;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.thomasricouard.IceCubesApp.IceCubesNotifications;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
9FBFE646292A715600C250E9 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
|
@ -460,6 +631,7 @@
|
|||
9FBFE649292A715600C250E9 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "AppIconAlternate1 AppIconAlternate2 AppIconAlternate3 AppIconAlternate4 AppIconAlternate5 AppIconAlternate6 AppIconAlternate7 AppIconAlternate8 AppIconAlternate9 AppIconAlternate10 AppIconAlternate11 AppIconAlternate12 AppIconAlternate13 AppIconAlternate14 AppIconAlternate15 AppIconAlternate16 AppIconAlternate17 AppIconAlternate19 AppIconAlternate18";
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
|
@ -509,6 +681,7 @@
|
|||
9FBFE64A292A715600C250E9 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "AppIconAlternate1 AppIconAlternate2 AppIconAlternate3 AppIconAlternate4 AppIconAlternate5 AppIconAlternate6 AppIconAlternate7 AppIconAlternate8 AppIconAlternate9 AppIconAlternate10 AppIconAlternate11 AppIconAlternate12 AppIconAlternate13 AppIconAlternate14 AppIconAlternate15 AppIconAlternate16 AppIconAlternate17 AppIconAlternate19 AppIconAlternate18";
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
|
@ -558,6 +731,15 @@
|
|||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
9F2A541E296AB631009B2D7C /* Build configuration list for PBXNativeTarget "IceCubesNotifications" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
9F2A541F296AB631009B2D7C /* Debug */,
|
||||
9F2A5420296AB631009B2D7C /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
9FBFE634292A715500C250E9 /* Build configuration list for PBXProject "IceCubesApp" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
|
@ -612,6 +794,19 @@
|
|||
package = 9F2A540829699705009B2D7C /* XCRemoteSwiftPackageReference "purchases-ios" */;
|
||||
productName = RevenueCat;
|
||||
};
|
||||
9F2A5423296AB67A009B2D7C /* Env */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = Env;
|
||||
};
|
||||
9F2A5425296AB67E009B2D7C /* KeychainSwift */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 9FAE4ACC29379A5A00772766 /* XCRemoteSwiftPackageReference "keychain-swift" */;
|
||||
productName = KeychainSwift;
|
||||
};
|
||||
9F2A5427296AB683009B2D7C /* Models */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = Models;
|
||||
};
|
||||
9F35DB43294F9A7D00B3281A /* Status */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = Status;
|
||||
|
|
|
@ -3,6 +3,7 @@ import Timeline
|
|||
import Network
|
||||
import KeychainSwift
|
||||
import Models
|
||||
import CryptoKit
|
||||
|
||||
struct AppAccount: Codable, Identifiable {
|
||||
let server: String
|
||||
|
@ -20,6 +21,11 @@ struct AppAccount: Codable, Identifiable {
|
|||
}
|
||||
}
|
||||
|
||||
internal init(server: String, oauthToken: OauthToken? = nil) {
|
||||
self.server = server
|
||||
self.oauthToken = oauthToken
|
||||
}
|
||||
|
||||
func save() throws {
|
||||
let encoder = JSONEncoder()
|
||||
let data = try encoder.encode(self)
|
||||
|
@ -31,17 +37,18 @@ struct AppAccount: Codable, Identifiable {
|
|||
KeychainSwift().delete(key)
|
||||
}
|
||||
|
||||
static func retrieveAll() throws -> [AppAccount] {
|
||||
static func retrieveAll() -> [AppAccount] {
|
||||
let keychain = KeychainSwift()
|
||||
let decoder = JSONDecoder()
|
||||
let keys = keychain.allKeys
|
||||
var accounts: [AppAccount] = []
|
||||
for key in keys {
|
||||
if let data = keychain.getData(key) {
|
||||
let account = try decoder.decode(AppAccount.self, from: data)
|
||||
if let account = try? decoder.decode(AppAccount.self, from: data) {
|
||||
accounts.append(account)
|
||||
}
|
||||
}
|
||||
}
|
||||
return accounts
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import SwiftUI
|
||||
import Network
|
||||
import Env
|
||||
|
||||
class AppAccountsManager: ObservableObject {
|
||||
@AppStorage("latestCurrentAccountKey") static public var latestCurrentAccountKey: String = ""
|
||||
|
@ -14,19 +15,22 @@ class AppAccountsManager: ObservableObject {
|
|||
@Published var availableAccounts: [AppAccount]
|
||||
@Published var currentClient: Client
|
||||
|
||||
init() {
|
||||
var pushAccounts: [PushNotifications.PushAccounts] {
|
||||
availableAccounts.filter{ $0.oauthToken != nil}
|
||||
.map{ .init(server: $0.server, token: $0.oauthToken!) }
|
||||
}
|
||||
|
||||
static var shared = AppAccountsManager()
|
||||
|
||||
private init() {
|
||||
var defaultAccount = AppAccount(server: IceCubesApp.defaultServer, oauthToken: nil)
|
||||
do {
|
||||
let keychainAccounts = try AppAccount.retrieveAll()
|
||||
let keychainAccounts = AppAccount.retrieveAll()
|
||||
availableAccounts = keychainAccounts
|
||||
if let currentAccount = keychainAccounts.first(where: { $0.id == Self.latestCurrentAccountKey }) {
|
||||
defaultAccount = currentAccount
|
||||
} else {
|
||||
defaultAccount = keychainAccounts.last ?? defaultAccount
|
||||
}
|
||||
} catch {
|
||||
availableAccounts = [defaultAccount]
|
||||
}
|
||||
currentAccount = defaultAccount
|
||||
currentClient = .init(server: defaultAccount.server, oauthToken: defaultAccount.oauthToken)
|
||||
}
|
||||
|
|
|
@ -11,8 +11,10 @@ import RevenueCat
|
|||
struct IceCubesApp: App {
|
||||
public static let defaultServer = "mastodon.social"
|
||||
|
||||
@UIApplicationDelegateAdaptor private var appDelegate: AppDelegate
|
||||
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
@StateObject private var appAccountsManager = AppAccountsManager()
|
||||
@StateObject private var appAccountsManager = AppAccountsManager.shared
|
||||
@StateObject private var currentInstance = CurrentInstance()
|
||||
@StateObject private var currentAccount = CurrentAccount()
|
||||
@StateObject private var userPreferences = UserPreferences()
|
||||
|
@ -36,6 +38,7 @@ struct IceCubesApp: App {
|
|||
setNewClientsInEnv(client: appAccountsManager.currentClient)
|
||||
setBarsColor(color: theme.primaryBackgroundColor)
|
||||
setupRevenueCat()
|
||||
refreshPushSubs()
|
||||
}
|
||||
.preferredColorScheme(theme.selectedScheme == ColorScheme.dark ? .dark : .light)
|
||||
.environmentObject(appAccountsManager)
|
||||
|
@ -46,6 +49,7 @@ struct IceCubesApp: App {
|
|||
.environmentObject(userPreferences)
|
||||
.environmentObject(theme)
|
||||
.environmentObject(watcher)
|
||||
.environmentObject(PushNotifications.shared)
|
||||
.quickLookPreview($quickLook.url, in: quickLook.urls)
|
||||
}
|
||||
.onChange(of: scenePhase, perform: { scenePhase in
|
||||
|
@ -148,4 +152,30 @@ struct IceCubesApp: App {
|
|||
Purchases.logLevel = .error
|
||||
Purchases.configure(withAPIKey: "appl_JXmiRckOzXXTsHKitQiicXCvMQi")
|
||||
}
|
||||
|
||||
private func refreshPushSubs() {
|
||||
PushNotifications.shared.requestPushNotifications()
|
||||
Task {
|
||||
await PushNotifications.shared.fetchSubscriptions(accounts: appAccountsManager.pushAccounts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
func application(_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication,
|
||||
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
||||
PushNotifications.shared.pushToken = deviceToken
|
||||
Task {
|
||||
await PushNotifications.shared.updateSubscriptions(accounts: AppAccountsManager.shared.pushAccounts)
|
||||
}
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ struct AddAccountView: View {
|
|||
@EnvironmentObject private var appAccountsManager: AppAccountsManager
|
||||
@EnvironmentObject private var currentAccount: CurrentAccount
|
||||
@EnvironmentObject private var currentInstance: CurrentInstance
|
||||
@EnvironmentObject private var pushNotifications: PushNotifications
|
||||
@EnvironmentObject private var theme: Theme
|
||||
|
||||
@State private var instanceName: String = ""
|
||||
|
@ -144,6 +145,9 @@ struct AddAccountView: View {
|
|||
do {
|
||||
let oauthToken = try await client.continueOauthFlow(url: url)
|
||||
appAccountsManager.add(account: AppAccount(server: client.server, oauthToken: oauthToken))
|
||||
Task {
|
||||
await pushNotifications.updateSubscriptions(accounts: appAccountsManager.pushAccounts)
|
||||
}
|
||||
isSigninIn = false
|
||||
dismiss()
|
||||
} catch {
|
||||
|
|
82
IceCubesApp/App/Tabs/Settings/PushNotificationsView.swift
Normal file
82
IceCubesApp/App/Tabs/Settings/PushNotificationsView.swift
Normal file
|
@ -0,0 +1,82 @@
|
|||
import SwiftUI
|
||||
import Models
|
||||
import DesignSystem
|
||||
import NukeUI
|
||||
import Network
|
||||
import UserNotifications
|
||||
import Env
|
||||
|
||||
struct PushNotificationsView: View {
|
||||
@EnvironmentObject private var theme: Theme
|
||||
@EnvironmentObject private var appAccountsManager: AppAccountsManager
|
||||
@EnvironmentObject private var pushNotifications: PushNotifications
|
||||
|
||||
@State private var subscriptions: [PushSubscription] = []
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
Toggle(isOn: $pushNotifications.isPushEnabled) {
|
||||
Text("Push notification")
|
||||
}
|
||||
Group {
|
||||
Toggle(isOn: $pushNotifications.isFollowNotificationEnabled) {
|
||||
Text("Follow notification")
|
||||
}
|
||||
Toggle(isOn: $pushNotifications.isFavoriteNotificationEnabled) {
|
||||
Text("Favorite notification")
|
||||
}
|
||||
Toggle(isOn: $pushNotifications.isReblogNotificationEnabled) {
|
||||
Text("Boost notification")
|
||||
}
|
||||
Toggle(isOn: $pushNotifications.isMentionNotificationEnabled) {
|
||||
Text("Mention notification")
|
||||
}
|
||||
Toggle(isOn: $pushNotifications.isPollNotificationEnabled) {
|
||||
Text("Polls notification")
|
||||
}
|
||||
}.disabled(!pushNotifications.isPushEnabled)
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
}
|
||||
.navigationTitle("Push Notifications")
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.onAppear {
|
||||
Task {
|
||||
await pushNotifications.fetchSubscriptions(accounts: appAccountsManager.pushAccounts)
|
||||
}
|
||||
}
|
||||
.onChange(of: pushNotifications.isPushEnabled) { newValue in
|
||||
pushNotifications.isUserPushEnabled = newValue
|
||||
if !newValue {
|
||||
Task {
|
||||
await pushNotifications.deleteSubscriptions(accounts: appAccountsManager.pushAccounts)
|
||||
}
|
||||
} else {
|
||||
updateSubscriptions()
|
||||
}
|
||||
}
|
||||
.onChange(of: pushNotifications.isFollowNotificationEnabled) { _ in
|
||||
updateSubscriptions()
|
||||
}
|
||||
.onChange(of: pushNotifications.isPollNotificationEnabled) { _ in
|
||||
updateSubscriptions()
|
||||
}
|
||||
.onChange(of: pushNotifications.isReblogNotificationEnabled) { _ in
|
||||
updateSubscriptions()
|
||||
}
|
||||
.onChange(of: pushNotifications.isMentionNotificationEnabled) { _ in
|
||||
updateSubscriptions()
|
||||
}
|
||||
.onChange(of: pushNotifications.isFavoriteNotificationEnabled) { _ in
|
||||
updateSubscriptions()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateSubscriptions() {
|
||||
Task {
|
||||
await pushNotifications.updateSubscriptions(accounts: appAccountsManager.pushAccounts)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -68,6 +68,9 @@ struct SettingsTabs: View {
|
|||
@ViewBuilder
|
||||
private var generalSection: some View {
|
||||
Section("General") {
|
||||
NavigationLink(destination: PushNotificationsView()) {
|
||||
Label("Push notifications", systemImage: "bell.and.waves.left.and.right")
|
||||
}
|
||||
if let instanceData = currentInstance.instance {
|
||||
NavigationLink(destination: InstanceInfoView(instance: instanceData)) {
|
||||
Label("Instance Information", systemImage: "server.rack")
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
|
|
10
IceCubesNotifications/IceCubesNotifications.entitlements
Normal file
10
IceCubesNotifications/IceCubesNotifications.entitlements
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)com.thomasricouard.IceCubesApp</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
13
IceCubesNotifications/Info.plist
Normal file
13
IceCubesNotifications/Info.plist
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.usernotifications.service</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).NotificationService</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
141
IceCubesNotifications/NotificationService.swift
Normal file
141
IceCubesNotifications/NotificationService.swift
Normal file
|
@ -0,0 +1,141 @@
|
|||
import UserNotifications
|
||||
import KeychainSwift
|
||||
import Env
|
||||
import CryptoKit
|
||||
import Models
|
||||
|
||||
@MainActor
|
||||
class NotificationService: UNNotificationServiceExtension {
|
||||
|
||||
var contentHandler: ((UNNotificationContent) -> Void)?
|
||||
var bestAttemptContent: UNMutableNotificationContent?
|
||||
|
||||
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
||||
self.contentHandler = contentHandler
|
||||
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
|
||||
bestAttemptContent?.title = "A new notification have been received"
|
||||
|
||||
if let bestAttemptContent {
|
||||
let privateKey = PushNotifications.shared.notificationsPrivateKeyAsKey
|
||||
let auth = PushNotifications.shared.notificationsAuthKeyAsKey
|
||||
|
||||
guard let encodedPayload = bestAttemptContent.userInfo["m"] as? String,
|
||||
let payload = Data(base64Encoded: encodedPayload.URLSafeBase64ToBase64()) else {
|
||||
contentHandler(bestAttemptContent)
|
||||
return
|
||||
}
|
||||
|
||||
guard let encodedPublicKey = bestAttemptContent.userInfo["k"] as? String,
|
||||
let publicKeyData = Data(base64Encoded: encodedPublicKey.URLSafeBase64ToBase64()),
|
||||
let publicKey = try? P256.KeyAgreement.PublicKey(x963Representation: publicKeyData) else {
|
||||
contentHandler(bestAttemptContent)
|
||||
return
|
||||
}
|
||||
|
||||
guard let encodedSalt = bestAttemptContent.userInfo["s"] as? String,
|
||||
let salt = Data(base64Encoded: encodedSalt.URLSafeBase64ToBase64()) else {
|
||||
contentHandler(bestAttemptContent)
|
||||
return
|
||||
}
|
||||
|
||||
guard let plaintextData = NotificationService.decrypt(payload: payload,
|
||||
salt: salt,
|
||||
auth: auth,
|
||||
privateKey: privateKey,
|
||||
publicKey: publicKey),
|
||||
let notification = try? JSONDecoder().decode(MastodonPushNotification.self, from: plaintextData) else {
|
||||
contentHandler(bestAttemptContent)
|
||||
return
|
||||
}
|
||||
|
||||
bestAttemptContent.title = notification.title
|
||||
bestAttemptContent.subtitle = ""
|
||||
bestAttemptContent.body = notification.body.escape()
|
||||
bestAttemptContent.userInfo["plaintext"] = plaintextData
|
||||
|
||||
contentHandler(bestAttemptContent)
|
||||
}
|
||||
}
|
||||
|
||||
static func decrypt(payload: Data, salt: Data, auth: Data, privateKey: P256.KeyAgreement.PrivateKey, publicKey: P256.KeyAgreement.PublicKey) -> Data? {
|
||||
guard let sharedSecret = try? privateKey.sharedSecretFromKeyAgreement(with: publicKey) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let keyMaterial = sharedSecret.hkdfDerivedSymmetricKey(using: SHA256.self, salt: auth, sharedInfo: Data("Content-Encoding: auth\0".utf8), outputByteCount: 32)
|
||||
|
||||
let keyInfo = info(type: "aesgcm", clientPublicKey: privateKey.publicKey.x963Representation, serverPublicKey: publicKey.x963Representation)
|
||||
let key = HKDF<SHA256>.deriveKey(inputKeyMaterial: keyMaterial, salt: salt, info: keyInfo, outputByteCount: 16)
|
||||
|
||||
let nonceInfo = info(type: "nonce", clientPublicKey: privateKey.publicKey.x963Representation, serverPublicKey: publicKey.x963Representation)
|
||||
let nonce = HKDF<SHA256>.deriveKey(inputKeyMaterial: keyMaterial, salt: salt, info: nonceInfo, outputByteCount: 12)
|
||||
|
||||
let nonceData = nonce.withUnsafeBytes(Array.init)
|
||||
|
||||
guard let sealedBox = try? AES.GCM.SealedBox(combined: nonceData + payload) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var _plaintext: Data?
|
||||
do {
|
||||
_plaintext = try AES.GCM.open(sealedBox, using: key)
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
guard let plaintext = _plaintext else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let paddingLength = Int(plaintext[0]) * 256 + Int(plaintext[1])
|
||||
guard plaintext.count >= 2 + paddingLength else {
|
||||
print("1")
|
||||
fatalError()
|
||||
}
|
||||
let unpadded = plaintext.suffix(from: paddingLength + 2)
|
||||
|
||||
return Data(unpadded)
|
||||
}
|
||||
|
||||
static private func info(type: String, clientPublicKey: Data, serverPublicKey: Data) -> Data {
|
||||
var info = Data()
|
||||
|
||||
info.append("Content-Encoding: ".data(using: .utf8)!)
|
||||
info.append(type.data(using: .utf8)!)
|
||||
info.append(0)
|
||||
info.append("P-256".data(using: .utf8)!)
|
||||
info.append(0)
|
||||
info.append(0)
|
||||
info.append(65)
|
||||
info.append(clientPublicKey)
|
||||
info.append(0)
|
||||
info.append(65)
|
||||
info.append(serverPublicKey)
|
||||
|
||||
return info
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
func escape() -> String {
|
||||
return self
|
||||
.replacingOccurrences(of: "&", with: "&")
|
||||
.replacingOccurrences(of: "<", with: "<")
|
||||
.replacingOccurrences(of: ">", with: ">")
|
||||
.replacingOccurrences(of: """, with: "\"")
|
||||
.replacingOccurrences(of: "'", with: "'")
|
||||
.replacingOccurrences(of: "'", with: "’")
|
||||
|
||||
}
|
||||
|
||||
func URLSafeBase64ToBase64() -> String {
|
||||
var base64 = replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
|
||||
let countMod4 = count % 4
|
||||
|
||||
if countMod4 != 0 {
|
||||
base64.append(String(repeating: "=", count: 4 - countMod4))
|
||||
}
|
||||
|
||||
return base64
|
||||
}
|
||||
}
|
||||
|
170
Packages/Env/Sources/Env/PushNotifications.swift
Normal file
170
Packages/Env/Sources/Env/PushNotifications.swift
Normal file
|
@ -0,0 +1,170 @@
|
|||
import Foundation
|
||||
import UserNotifications
|
||||
import SwiftUI
|
||||
import KeychainSwift
|
||||
import CryptoKit
|
||||
import Models
|
||||
import Network
|
||||
|
||||
@MainActor
|
||||
public class PushNotifications: ObservableObject {
|
||||
enum Constants {
|
||||
static let endpoint = "https://icecubesrelay.fly.dev"
|
||||
static let keychainGroup = "Z6P74P6T99.com.thomasricouard.IceCubesApp"
|
||||
static let keychainAuthKey = "notifications_auth_key"
|
||||
static let keychainPrivateKey = "notifications_private_key"
|
||||
}
|
||||
|
||||
public struct PushAccounts {
|
||||
public let server: String
|
||||
public let token: OauthToken
|
||||
|
||||
public init(server: String, token: OauthToken) {
|
||||
self.server = server
|
||||
self.token = token
|
||||
}
|
||||
}
|
||||
|
||||
public static let shared = PushNotifications()
|
||||
|
||||
@Published public var pushToken: Data?
|
||||
|
||||
@AppStorage("user_push_is_on") public var isUserPushEnabled: Bool = true
|
||||
@Published public var isPushEnabled: Bool = false {
|
||||
didSet {
|
||||
if !oldValue && isPushEnabled {
|
||||
requestPushNotifications()
|
||||
}
|
||||
}
|
||||
}
|
||||
@Published public var isFollowNotificationEnabled: Bool = true
|
||||
@Published public var isFavoriteNotificationEnabled: Bool = true
|
||||
@Published public var isReblogNotificationEnabled: Bool = true
|
||||
@Published public var isMentionNotificationEnabled: Bool = true
|
||||
@Published public var isPollNotificationEnabled: Bool = true
|
||||
|
||||
private var subscriptions: [PushSubscription] = []
|
||||
|
||||
private var keychain: KeychainSwift {
|
||||
let keychain = KeychainSwift()
|
||||
keychain.accessGroup = Constants.keychainGroup
|
||||
return keychain
|
||||
}
|
||||
|
||||
public func requestPushNotifications() {
|
||||
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { (_, _) in
|
||||
DispatchQueue.main.async {
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func fetchSubscriptions(accounts: [PushAccounts]) async {
|
||||
subscriptions = []
|
||||
for account in accounts {
|
||||
let client = Client(server: account.server, oauthToken: account.token)
|
||||
do {
|
||||
let sub: PushSubscription = try await client.get(endpoint: Push.subscription)
|
||||
subscriptions.append(sub)
|
||||
} catch { }
|
||||
}
|
||||
refreshSubscriptionsUI()
|
||||
}
|
||||
|
||||
public func updateSubscriptions(accounts: [PushAccounts]) async {
|
||||
subscriptions = []
|
||||
guard let pushToken = pushToken, isUserPushEnabled else { return }
|
||||
for account in accounts {
|
||||
let client = Client(server: account.server, oauthToken: account.token)
|
||||
do {
|
||||
var listenerURL = Constants.endpoint
|
||||
listenerURL += "/push/"
|
||||
listenerURL += pushToken.hexString
|
||||
listenerURL += "/\(account.server)"
|
||||
#if DEBUG
|
||||
listenerURL += "?sandbox=true"
|
||||
#endif
|
||||
let sub: PushSubscription =
|
||||
try await client.post(endpoint: Push.createSub(endpoint: listenerURL,
|
||||
p256dh: notificationsPrivateKeyAsKey.publicKey.x963Representation,
|
||||
auth: notificationsAuthKeyAsKey,
|
||||
mentions: isMentionNotificationEnabled,
|
||||
status: true,
|
||||
reblog: isReblogNotificationEnabled,
|
||||
follow: isFollowNotificationEnabled,
|
||||
favourite: isFavoriteNotificationEnabled,
|
||||
poll: isPollNotificationEnabled))
|
||||
subscriptions.append(sub)
|
||||
} catch { }
|
||||
}
|
||||
refreshSubscriptionsUI()
|
||||
}
|
||||
|
||||
public func deleteSubscriptions(accounts: [PushAccounts]) async {
|
||||
for account in accounts {
|
||||
let client = Client(server: account.server, oauthToken: account.token)
|
||||
do {
|
||||
_ = try await client.delete(endpoint: Push.subscription)
|
||||
} catch { }
|
||||
}
|
||||
await fetchSubscriptions(accounts: accounts)
|
||||
refreshSubscriptionsUI()
|
||||
}
|
||||
|
||||
private func refreshSubscriptionsUI() {
|
||||
if let sub = subscriptions.first {
|
||||
isPushEnabled = true
|
||||
isFollowNotificationEnabled = sub.alerts.follow
|
||||
isFavoriteNotificationEnabled = sub.alerts.favourite
|
||||
isReblogNotificationEnabled = sub.alerts.reblog
|
||||
isMentionNotificationEnabled = sub.alerts.mention
|
||||
isPollNotificationEnabled = sub.alerts.poll
|
||||
} else {
|
||||
isPushEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Key management
|
||||
|
||||
public var notificationsPrivateKeyAsKey: P256.KeyAgreement.PrivateKey {
|
||||
if let key = keychain.get(Constants.keychainPrivateKey),
|
||||
let data = Data(base64Encoded: key) {
|
||||
do {
|
||||
return try P256.KeyAgreement.PrivateKey(rawRepresentation: data)
|
||||
} catch {
|
||||
let key = P256.KeyAgreement.PrivateKey()
|
||||
keychain.set(key.rawRepresentation.base64EncodedString(), forKey: Constants.keychainPrivateKey)
|
||||
return key
|
||||
}
|
||||
} else {
|
||||
let key = P256.KeyAgreement.PrivateKey()
|
||||
keychain.set(key.rawRepresentation.base64EncodedString(), forKey: Constants.keychainPrivateKey)
|
||||
return key
|
||||
}
|
||||
}
|
||||
|
||||
public var notificationsAuthKeyAsKey: Data {
|
||||
if let key = keychain.get(Constants.keychainAuthKey),
|
||||
let data = Data(base64Encoded: key) {
|
||||
return data
|
||||
} else {
|
||||
let key = Self.makeRandomeNotificationsAuthKey()
|
||||
keychain.set(key.base64EncodedString(), forKey: Constants.keychainAuthKey)
|
||||
return key
|
||||
}
|
||||
}
|
||||
|
||||
static private func makeRandomeNotificationsAuthKey() -> Data {
|
||||
let byteCount = 16
|
||||
var bytes = Data(count: byteCount)
|
||||
_ = bytes.withUnsafeMutableBytes { SecRandomCopyBytes(kSecRandomDefault, byteCount, $0.baseAddress!) }
|
||||
return bytes
|
||||
}
|
||||
}
|
||||
|
||||
extension Data {
|
||||
var hexString: String {
|
||||
return map { String(format: "%02.2hhx", arguments: [$0]) }.joined()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import Foundation
|
||||
|
||||
public struct MastodonPushNotification: Codable {
|
||||
|
||||
public let accessToken: String
|
||||
|
||||
public let notificationID: Int
|
||||
public let notificationType: String
|
||||
|
||||
public let preferredLocale: String?
|
||||
public let icon: String?
|
||||
public let title: String
|
||||
public let body: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case accessToken = "access_token"
|
||||
case notificationID = "notification_id"
|
||||
case notificationType = "notification_type"
|
||||
case preferredLocale = "preferred_locale"
|
||||
case icon
|
||||
case title
|
||||
case body
|
||||
}
|
||||
}
|
16
Packages/Models/Sources/Models/PushSubscription.swift
Normal file
16
Packages/Models/Sources/Models/PushSubscription.swift
Normal file
|
@ -0,0 +1,16 @@
|
|||
import Foundation
|
||||
|
||||
public struct PushSubscription: Identifiable, Decodable {
|
||||
public struct Alerts: Decodable {
|
||||
public let follow: Bool
|
||||
public let favourite: Bool
|
||||
public let reblog: Bool
|
||||
public let mention: Bool
|
||||
public let poll: Bool
|
||||
}
|
||||
|
||||
public let id: Int
|
||||
public let endpoint: URL
|
||||
public let serverKey: String
|
||||
public let alerts: Alerts
|
||||
}
|
41
Packages/Network/Sources/Network/Endpoint/Push.swift
Normal file
41
Packages/Network/Sources/Network/Endpoint/Push.swift
Normal file
|
@ -0,0 +1,41 @@
|
|||
import Foundation
|
||||
|
||||
public enum Push: Endpoint {
|
||||
case subscription
|
||||
case createSub(endpoint: String,
|
||||
p256dh: Data,
|
||||
auth: Data,
|
||||
mentions: Bool,
|
||||
status: Bool,
|
||||
reblog: Bool,
|
||||
follow: Bool,
|
||||
favourite: Bool,
|
||||
poll: Bool)
|
||||
|
||||
public func path() -> String {
|
||||
switch self {
|
||||
case .subscription, .createSub:
|
||||
return "push/subscription"
|
||||
}
|
||||
}
|
||||
|
||||
public func queryItems() -> [URLQueryItem]? {
|
||||
switch self {
|
||||
case let .createSub(endpoint, p256dh, auth, mentions, status, reblog, follow, favourite, poll):
|
||||
var params: [URLQueryItem] = []
|
||||
params.append(.init(name: "subscription[endpoint]", value: endpoint))
|
||||
params.append(.init(name: "subscription[keys][p256dh]", value: p256dh.base64UrlEncodedString()))
|
||||
params.append(.init(name: "subscription[keys][auth]", value: auth.base64UrlEncodedString()))
|
||||
params.append(.init(name: "data[alerts][mention]", value: mentions ? "true" : "false"))
|
||||
params.append(.init(name: "data[alerts][status]", value: status ? "true" : "false"))
|
||||
params.append(.init(name: "data[alerts][follow]", value: follow ? "true" : "false"))
|
||||
params.append(.init(name: "data[alerts][reblog]", value: reblog ? "true" : "false"))
|
||||
params.append(.init(name: "data[alerts][favourite]", value: favourite ? "true" : "false"))
|
||||
params.append(.init(name: "data[alerts][poll]", value: poll ? "true" : "false"))
|
||||
params.append(.init(name: "policy", value: "all"))
|
||||
return params
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
10
Packages/Network/Sources/Network/URLData.swift
Normal file
10
Packages/Network/Sources/Network/URLData.swift
Normal file
|
@ -0,0 +1,10 @@
|
|||
import Foundation
|
||||
|
||||
extension Data {
|
||||
func base64UrlEncodedString() -> String {
|
||||
return base64EncodedString()
|
||||
.replacingOccurrences(of: "+", with: "-")
|
||||
.replacingOccurrences(of: "/", with: "_")
|
||||
.replacingOccurrences(of: "=", with: "")
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue