mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-25 17:51:01 +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 */; };
|
9F2A540A29699705009B2D7C /* ReceiptParser in Frameworks */ = {isa = PBXBuildFile; productRef = 9F2A540929699705009B2D7C /* ReceiptParser */; };
|
||||||
9F2A540C29699705009B2D7C /* RevenueCat in Frameworks */ = {isa = PBXBuildFile; productRef = 9F2A540B29699705009B2D7C /* RevenueCat */; };
|
9F2A540C29699705009B2D7C /* RevenueCat in Frameworks */ = {isa = PBXBuildFile; productRef = 9F2A540B29699705009B2D7C /* RevenueCat */; };
|
||||||
9F2A540E2969A0B0009B2D7C /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F2A540D2969A0B0009B2D7C /* StoreKit.framework */; };
|
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 */; };
|
9F2B92F6295AE04800DE16D0 /* Tabs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2B92F5295AE04800DE16D0 /* Tabs.swift */; };
|
||||||
9F2B92FA295DA7D700DE16D0 /* AddAccountsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2B92F9295DA7D700DE16D0 /* AddAccountsView.swift */; };
|
9F2B92FA295DA7D700DE16D0 /* AddAccountsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2B92F9295DA7D700DE16D0 /* AddAccountsView.swift */; };
|
||||||
9F2B92FC295DA94500DE16D0 /* InstanceInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2B92FB295DA94500DE16D0 /* InstanceInfoView.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 */; };
|
9FE151A6293C90F900E9683D /* IconSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE151A5293C90F900E9683D /* IconSelectorView.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* 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 */
|
/* Begin PBXFileReference section */
|
||||||
9F24EEB729360C330042359D /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
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>"; };
|
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; };
|
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>"; };
|
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; };
|
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>"; };
|
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>"; };
|
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>"; };
|
9F2B92FB295DA94500DE16D0 /* InstanceInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceInfoView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -89,6 +124,16 @@
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase 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 */ = {
|
9FBFE636292A715500C250E9 /* Frameworks */ = {
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
|
@ -115,6 +160,16 @@
|
||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
|
9F2A5417296AB631009B2D7C /* IceCubesNotifications */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
9F2A5422296AB64B009B2D7C /* IceCubesNotifications.entitlements */,
|
||||||
|
9F2A5418296AB631009B2D7C /* NotificationService.swift */,
|
||||||
|
9F2A541A296AB631009B2D7C /* Info.plist */,
|
||||||
|
);
|
||||||
|
path = IceCubesNotifications;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
9F398AB429360A5800A889F2 /* App */ = {
|
9F398AB429360A5800A889F2 /* App */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -172,6 +227,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
9FBFE63B292A715500C250E9 /* IceCubesApp */,
|
9FBFE63B292A715500C250E9 /* IceCubesApp */,
|
||||||
|
9F2A5417296AB631009B2D7C /* IceCubesNotifications */,
|
||||||
9FBFE63A292A715500C250E9 /* Products */,
|
9FBFE63A292A715500C250E9 /* Products */,
|
||||||
9FBFE64C292A72BD00C250E9 /* Frameworks */,
|
9FBFE64C292A72BD00C250E9 /* Frameworks */,
|
||||||
9F398AAC2936005300A889F2 /* Account */,
|
9F398AAC2936005300A889F2 /* Account */,
|
||||||
|
@ -192,6 +248,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
9FBFE639292A715500C250E9 /* IceCubesApp.app */,
|
9FBFE639292A715500C250E9 /* IceCubesApp.app */,
|
||||||
|
9F2A5416296AB631009B2D7C /* IceCubesNotifications.appex */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -228,6 +285,7 @@
|
||||||
9F2B92FB295DA94500DE16D0 /* InstanceInfoView.swift */,
|
9F2B92FB295DA94500DE16D0 /* InstanceInfoView.swift */,
|
||||||
9F7335F82968576500AFF0BA /* DisplaySettingsView.swift */,
|
9F7335F82968576500AFF0BA /* DisplaySettingsView.swift */,
|
||||||
9F2A540629699698009B2D7C /* SupportAppView.swift */,
|
9F2A540629699698009B2D7C /* SupportAppView.swift */,
|
||||||
|
9F2A5410296A1429009B2D7C /* PushNotificationsView.swift */,
|
||||||
);
|
);
|
||||||
path = Settings;
|
path = Settings;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -235,6 +293,28 @@
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget 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 */ = {
|
9FBFE638292A715500C250E9 /* IceCubesApp */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 9FBFE648292A715600C250E9 /* Build configuration list for PBXNativeTarget "IceCubesApp" */;
|
buildConfigurationList = 9FBFE648292A715600C250E9 /* Build configuration list for PBXNativeTarget "IceCubesApp" */;
|
||||||
|
@ -242,10 +322,12 @@
|
||||||
9FBFE635292A715500C250E9 /* Sources */,
|
9FBFE635292A715500C250E9 /* Sources */,
|
||||||
9FBFE636292A715500C250E9 /* Frameworks */,
|
9FBFE636292A715500C250E9 /* Frameworks */,
|
||||||
9FBFE637292A715500C250E9 /* Resources */,
|
9FBFE637292A715500C250E9 /* Resources */,
|
||||||
|
9F2A5421296AB631009B2D7C /* Embed Foundation Extensions */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
dependencies = (
|
dependencies = (
|
||||||
|
9F2A541C296AB631009B2D7C /* PBXTargetDependency */,
|
||||||
);
|
);
|
||||||
name = IceCubesApp;
|
name = IceCubesApp;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
|
@ -274,9 +356,12 @@
|
||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
BuildIndependentTargetsInParallel = 1;
|
BuildIndependentTargetsInParallel = 1;
|
||||||
LastSwiftUpdateCheck = 1410;
|
LastSwiftUpdateCheck = 1420;
|
||||||
LastUpgradeCheck = 1420;
|
LastUpgradeCheck = 1420;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
|
9F2A5415296AB631009B2D7C = {
|
||||||
|
CreatedOnToolsVersion = 14.2;
|
||||||
|
};
|
||||||
9FBFE638292A715500C250E9 = {
|
9FBFE638292A715500C250E9 = {
|
||||||
CreatedOnToolsVersion = 14.1;
|
CreatedOnToolsVersion = 14.1;
|
||||||
};
|
};
|
||||||
|
@ -300,11 +385,19 @@
|
||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
targets = (
|
targets = (
|
||||||
9FBFE638292A715500C250E9 /* IceCubesApp */,
|
9FBFE638292A715500C250E9 /* IceCubesApp */,
|
||||||
|
9F2A5415296AB631009B2D7C /* IceCubesNotifications */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
|
|
||||||
/* Begin PBXResourcesBuildPhase section */
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
9F2A5414296AB631009B2D7C /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
9FBFE637292A715500C250E9 /* Resources */ = {
|
9FBFE637292A715500C250E9 /* Resources */ = {
|
||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
|
@ -317,6 +410,14 @@
|
||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
9F2A5412296AB631009B2D7C /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
9F2A5419296AB631009B2D7C /* NotificationService.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
9FBFE635292A715500C250E9 /* Sources */ = {
|
9FBFE635292A715500C250E9 /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
|
@ -340,12 +441,82 @@
|
||||||
9F7335F72968274500AFF0BA /* AppAccountsSelectorView.swift in Sources */,
|
9F7335F72968274500AFF0BA /* AppAccountsSelectorView.swift in Sources */,
|
||||||
9FAE4AD129379AD600772766 /* AppAccount.swift in Sources */,
|
9FAE4AD129379AD600772766 /* AppAccount.swift in Sources */,
|
||||||
9F55C68D2955968700F94077 /* ExploreTab.swift in Sources */,
|
9F55C68D2955968700F94077 /* ExploreTab.swift in Sources */,
|
||||||
|
9F2A5411296A1429009B2D7C /* PushNotificationsView.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXTargetDependency section */
|
||||||
|
9F2A541C296AB631009B2D7C /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = 9F2A5415296AB631009B2D7C /* IceCubesNotifications */;
|
||||||
|
targetProxy = 9F2A541B296AB631009B2D7C /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration 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 */ = {
|
9FBFE646292A715600C250E9 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
@ -460,6 +631,7 @@
|
||||||
9FBFE649292A715600C250E9 /* Debug */ = {
|
9FBFE649292A715600C250E9 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
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_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_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
@ -509,6 +681,7 @@
|
||||||
9FBFE64A292A715600C250E9 /* Release */ = {
|
9FBFE64A292A715600C250E9 /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
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_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_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
@ -558,6 +731,15 @@
|
||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList 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" */ = {
|
9FBFE634292A715500C250E9 /* Build configuration list for PBXProject "IceCubesApp" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
|
@ -612,6 +794,19 @@
|
||||||
package = 9F2A540829699705009B2D7C /* XCRemoteSwiftPackageReference "purchases-ios" */;
|
package = 9F2A540829699705009B2D7C /* XCRemoteSwiftPackageReference "purchases-ios" */;
|
||||||
productName = RevenueCat;
|
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 */ = {
|
9F35DB43294F9A7D00B3281A /* Status */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = Status;
|
productName = Status;
|
||||||
|
|
|
@ -3,6 +3,7 @@ import Timeline
|
||||||
import Network
|
import Network
|
||||||
import KeychainSwift
|
import KeychainSwift
|
||||||
import Models
|
import Models
|
||||||
|
import CryptoKit
|
||||||
|
|
||||||
struct AppAccount: Codable, Identifiable {
|
struct AppAccount: Codable, Identifiable {
|
||||||
let server: String
|
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 {
|
func save() throws {
|
||||||
let encoder = JSONEncoder()
|
let encoder = JSONEncoder()
|
||||||
let data = try encoder.encode(self)
|
let data = try encoder.encode(self)
|
||||||
|
@ -31,15 +37,16 @@ struct AppAccount: Codable, Identifiable {
|
||||||
KeychainSwift().delete(key)
|
KeychainSwift().delete(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func retrieveAll() throws -> [AppAccount] {
|
static func retrieveAll() -> [AppAccount] {
|
||||||
let keychain = KeychainSwift()
|
let keychain = KeychainSwift()
|
||||||
let decoder = JSONDecoder()
|
let decoder = JSONDecoder()
|
||||||
let keys = keychain.allKeys
|
let keys = keychain.allKeys
|
||||||
var accounts: [AppAccount] = []
|
var accounts: [AppAccount] = []
|
||||||
for key in keys {
|
for key in keys {
|
||||||
if let data = keychain.getData(key) {
|
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)
|
accounts.append(account)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return accounts
|
return accounts
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Network
|
import Network
|
||||||
|
import Env
|
||||||
|
|
||||||
class AppAccountsManager: ObservableObject {
|
class AppAccountsManager: ObservableObject {
|
||||||
@AppStorage("latestCurrentAccountKey") static public var latestCurrentAccountKey: String = ""
|
@AppStorage("latestCurrentAccountKey") static public var latestCurrentAccountKey: String = ""
|
||||||
|
@ -14,18 +15,21 @@ class AppAccountsManager: ObservableObject {
|
||||||
@Published var availableAccounts: [AppAccount]
|
@Published var availableAccounts: [AppAccount]
|
||||||
@Published var currentClient: Client
|
@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)
|
var defaultAccount = AppAccount(server: IceCubesApp.defaultServer, oauthToken: nil)
|
||||||
do {
|
let keychainAccounts = AppAccount.retrieveAll()
|
||||||
let keychainAccounts = try AppAccount.retrieveAll()
|
availableAccounts = keychainAccounts
|
||||||
availableAccounts = keychainAccounts
|
if let currentAccount = keychainAccounts.first(where: { $0.id == Self.latestCurrentAccountKey }) {
|
||||||
if let currentAccount = keychainAccounts.first(where: { $0.id == Self.latestCurrentAccountKey }) {
|
defaultAccount = currentAccount
|
||||||
defaultAccount = currentAccount
|
} else {
|
||||||
} else {
|
defaultAccount = keychainAccounts.last ?? defaultAccount
|
||||||
defaultAccount = keychainAccounts.last ?? defaultAccount
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
availableAccounts = [defaultAccount]
|
|
||||||
}
|
}
|
||||||
currentAccount = defaultAccount
|
currentAccount = defaultAccount
|
||||||
currentClient = .init(server: defaultAccount.server, oauthToken: defaultAccount.oauthToken)
|
currentClient = .init(server: defaultAccount.server, oauthToken: defaultAccount.oauthToken)
|
||||||
|
|
|
@ -11,8 +11,10 @@ import RevenueCat
|
||||||
struct IceCubesApp: App {
|
struct IceCubesApp: App {
|
||||||
public static let defaultServer = "mastodon.social"
|
public static let defaultServer = "mastodon.social"
|
||||||
|
|
||||||
|
@UIApplicationDelegateAdaptor private var appDelegate: AppDelegate
|
||||||
|
|
||||||
@Environment(\.scenePhase) private var scenePhase
|
@Environment(\.scenePhase) private var scenePhase
|
||||||
@StateObject private var appAccountsManager = AppAccountsManager()
|
@StateObject private var appAccountsManager = AppAccountsManager.shared
|
||||||
@StateObject private var currentInstance = CurrentInstance()
|
@StateObject private var currentInstance = CurrentInstance()
|
||||||
@StateObject private var currentAccount = CurrentAccount()
|
@StateObject private var currentAccount = CurrentAccount()
|
||||||
@StateObject private var userPreferences = UserPreferences()
|
@StateObject private var userPreferences = UserPreferences()
|
||||||
|
@ -36,6 +38,7 @@ struct IceCubesApp: App {
|
||||||
setNewClientsInEnv(client: appAccountsManager.currentClient)
|
setNewClientsInEnv(client: appAccountsManager.currentClient)
|
||||||
setBarsColor(color: theme.primaryBackgroundColor)
|
setBarsColor(color: theme.primaryBackgroundColor)
|
||||||
setupRevenueCat()
|
setupRevenueCat()
|
||||||
|
refreshPushSubs()
|
||||||
}
|
}
|
||||||
.preferredColorScheme(theme.selectedScheme == ColorScheme.dark ? .dark : .light)
|
.preferredColorScheme(theme.selectedScheme == ColorScheme.dark ? .dark : .light)
|
||||||
.environmentObject(appAccountsManager)
|
.environmentObject(appAccountsManager)
|
||||||
|
@ -46,6 +49,7 @@ struct IceCubesApp: App {
|
||||||
.environmentObject(userPreferences)
|
.environmentObject(userPreferences)
|
||||||
.environmentObject(theme)
|
.environmentObject(theme)
|
||||||
.environmentObject(watcher)
|
.environmentObject(watcher)
|
||||||
|
.environmentObject(PushNotifications.shared)
|
||||||
.quickLookPreview($quickLook.url, in: quickLook.urls)
|
.quickLookPreview($quickLook.url, in: quickLook.urls)
|
||||||
}
|
}
|
||||||
.onChange(of: scenePhase, perform: { scenePhase in
|
.onChange(of: scenePhase, perform: { scenePhase in
|
||||||
|
@ -148,4 +152,30 @@ struct IceCubesApp: App {
|
||||||
Purchases.logLevel = .error
|
Purchases.logLevel = .error
|
||||||
Purchases.configure(withAPIKey: "appl_JXmiRckOzXXTsHKitQiicXCvMQi")
|
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 appAccountsManager: AppAccountsManager
|
||||||
@EnvironmentObject private var currentAccount: CurrentAccount
|
@EnvironmentObject private var currentAccount: CurrentAccount
|
||||||
@EnvironmentObject private var currentInstance: CurrentInstance
|
@EnvironmentObject private var currentInstance: CurrentInstance
|
||||||
|
@EnvironmentObject private var pushNotifications: PushNotifications
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
|
|
||||||
@State private var instanceName: String = ""
|
@State private var instanceName: String = ""
|
||||||
|
@ -144,6 +145,9 @@ struct AddAccountView: View {
|
||||||
do {
|
do {
|
||||||
let oauthToken = try await client.continueOauthFlow(url: url)
|
let oauthToken = try await client.continueOauthFlow(url: url)
|
||||||
appAccountsManager.add(account: AppAccount(server: client.server, oauthToken: oauthToken))
|
appAccountsManager.add(account: AppAccount(server: client.server, oauthToken: oauthToken))
|
||||||
|
Task {
|
||||||
|
await pushNotifications.updateSubscriptions(accounts: appAccountsManager.pushAccounts)
|
||||||
|
}
|
||||||
isSigninIn = false
|
isSigninIn = false
|
||||||
dismiss()
|
dismiss()
|
||||||
} catch {
|
} 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
|
@ViewBuilder
|
||||||
private var generalSection: some View {
|
private var generalSection: some View {
|
||||||
Section("General") {
|
Section("General") {
|
||||||
|
NavigationLink(destination: PushNotificationsView()) {
|
||||||
|
Label("Push notifications", systemImage: "bell.and.waves.left.and.right")
|
||||||
|
}
|
||||||
if let instanceData = currentInstance.instance {
|
if let instanceData = currentInstance.instance {
|
||||||
NavigationLink(destination: InstanceInfoView(instance: instanceData)) {
|
NavigationLink(destination: InstanceInfoView(instance: instanceData)) {
|
||||||
Label("Instance Information", systemImage: "server.rack")
|
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">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>aps-environment</key>
|
||||||
|
<string>development</string>
|
||||||
<key>com.apple.security.app-sandbox</key>
|
<key>com.apple.security.app-sandbox</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.files.user-selected.read-only</key>
|
<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