diff --git a/Purple.storekit b/Purple.storekit new file mode 100644 index 00000000..3a0c24d7 --- /dev/null +++ b/Purple.storekit @@ -0,0 +1,125 @@ +{ + "identifier" : "64C21A2D", + "nonRenewingSubscriptions" : [ + + ], + "products" : [ + + ], + "settings" : { + "_applicationInternalID" : "1628663131", + "_developerTeamID" : "XK7H4JAB3D", + "_failTransactionsEnabled" : false, + "_lastSynchronizedDate" : 704848066.26849198, + "_locale" : "en_US", + "_storefront" : "USA", + "_storeKitErrors" : [ + { + "current" : null, + "enabled" : false, + "name" : "Load Products" + }, + { + "current" : null, + "enabled" : false, + "name" : "Purchase" + }, + { + "current" : null, + "enabled" : false, + "name" : "Verification" + }, + { + "current" : null, + "enabled" : false, + "name" : "App Store Sync" + }, + { + "current" : null, + "enabled" : false, + "name" : "Subscription Status" + }, + { + "current" : null, + "enabled" : false, + "name" : "App Transaction" + }, + { + "current" : null, + "enabled" : false, + "name" : "Manage Subscriptions Sheet" + }, + { + "current" : null, + "enabled" : false, + "name" : "Refund Request Sheet" + }, + { + "current" : null, + "enabled" : false, + "name" : "Offer Code Redeem Sheet" + } + ] + }, + "subscriptionGroups" : [ + { + "id" : "21283177", + "localizations" : [ + + ], + "name" : "Purple", + "subscriptions" : [ + { + "adHocOffers" : [ + + ], + "codeOffers" : [ + + ], + "displayPrice" : "6.99", + "familyShareable" : false, + "groupNumber" : 1, + "internalID" : "6446591615", + "introductoryOffer" : null, + "localizations" : [ + { + "description" : "Support damus development with Damus Purple!", + "displayName" : "Damus Purple", + "locale" : "en_CA" + } + ], + "productID" : "purple", + "recurringSubscriptionPeriod" : "P1M", + "referenceName" : "Purple", + "subscriptionGroupID" : "21283177", + "type" : "RecurringSubscription" + }, + { + "adHocOffers" : [ + + ], + "codeOffers" : [ + + ], + "displayPrice" : "69.99", + "familyShareable" : false, + "groupNumber" : 2, + "internalID" : "6448764101", + "introductoryOffer" : null, + "localizations" : [ + + ], + "productID" : "purpleyearly", + "recurringSubscriptionPeriod" : "P1Y", + "referenceName" : "Purple Yearly", + "subscriptionGroupID" : "21283177", + "type" : "RecurringSubscription" + } + ] + } + ], + "version" : { + "major" : 3, + "minor" : 0 + } +} diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index 4e2dbb0c..30287665 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -261,6 +261,7 @@ 4C9BB83129C0ED4F00FC4E37 /* DisplayName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */; }; 4C9BB83429C12D9900FC4E37 /* EventProfileName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */; }; 4C9D6D1B2B1D35D7004E5CD9 /* PullDownSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9D6D1A2B1D35D7004E5CD9 /* PullDownSearch.swift */; }; + 4C9D6D162B1AA9C6004E5CD9 /* DisplayTabBarNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9D6D152B1AA9C6004E5CD9 /* DisplayTabBarNotify.swift */; }; 4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */; }; 4C9F18E429ABDE6D008C55EC /* MaybeAnonPfpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */; }; 4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */; }; @@ -374,6 +375,7 @@ 4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABF52985CD5500D66079 /* UserSearch.swift */; }; 4CF38C882A9442DC00BE01B6 /* UserStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF38C872A9442DC00BE01B6 /* UserStatusView.swift */; }; 4CFD502F2A2DA45800A229DB /* MediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFD502E2A2DA45800A229DB /* MediaView.swift */; }; + 4CFF8F5929C9FD1E008DB934 /* DamusPurpleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F5829C9FD1E008DB934 /* DamusPurpleView.swift */; }; 4CFF8F6329CC9AD7008DB934 /* ImageContextMenuModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6229CC9AD7008DB934 /* ImageContextMenuModifier.swift */; }; 4CFF8F6729CC9E3A008DB934 /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6629CC9E3A008DB934 /* ImageView.swift */; }; 4CFF8F6929CC9ED1008DB934 /* ImageContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6829CC9ED1008DB934 /* ImageContainerView.swift */; }; @@ -439,6 +441,9 @@ D72A2D072AD9C1FB002AFF62 /* MockProfiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */; }; D7315A2A2ACDF3B70036E30A /* DamusCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */; }; D7315A2C2ACDF4DA0036E30A /* DamusCacheManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */; }; + D74F430A2B23F0BE00425B75 /* DamusPurple.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F43092B23F0BE00425B75 /* DamusPurple.swift */; }; + D74F430C2B23FB9B00425B75 /* StoreObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F430B2B23FB9B00425B75 /* StoreObserver.swift */; }; + D76556D62B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D76556D52B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift */; }; D76874F32AE3632B00FB0F68 /* ProfileZapLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D76874F22AE3632B00FB0F68 /* ProfileZapLinkView.swift */; }; D77BFA0B2AE3051200621634 /* ProfileActionSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */; }; D783A63F2AD4E53D00658DDA /* SuggestedHashtagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */; }; @@ -1033,6 +1038,7 @@ 4C8682862814DE470026224F /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = ""; }; 4C86F7C32A76C44C00EC0817 /* ZappingNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZappingNotify.swift; sourceTree = ""; }; 4C86F7C52A76C51100EC0817 /* AttachedWalletNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachedWalletNotify.swift; sourceTree = ""; }; + 4C8AE1182A0320BE00B944E6 /* Purple.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = Purple.storekit; sourceTree = ""; }; 4C8D00C729DF791C0036AF10 /* CompatibleAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompatibleAttribute.swift; sourceTree = ""; }; 4C8D00C929DF80350036AF10 /* TruncatedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TruncatedText.swift; sourceTree = ""; }; 4C8D00CB29DF92DF0036AF10 /* Hashtags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hashtags.swift; sourceTree = ""; }; @@ -1059,6 +1065,7 @@ 4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayName.swift; sourceTree = ""; }; 4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventProfileName.swift; sourceTree = ""; }; 4C9D6D1A2B1D35D7004E5CD9 /* PullDownSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullDownSearch.swift; sourceTree = ""; }; + 4C9D6D152B1AA9C6004E5CD9 /* DisplayTabBarNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayTabBarNotify.swift; sourceTree = ""; }; 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeZapView.swift; sourceTree = ""; }; 4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaybeAnonPfpView.swift; sourceTree = ""; }; 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = ""; }; @@ -1184,6 +1191,7 @@ 4CF0ABF52985CD5500D66079 /* UserSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSearch.swift; sourceTree = ""; }; 4CF38C872A9442DC00BE01B6 /* UserStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserStatusView.swift; sourceTree = ""; }; 4CFD502E2A2DA45800A229DB /* MediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaView.swift; sourceTree = ""; }; + 4CFF8F5829C9FD1E008DB934 /* DamusPurpleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleView.swift; sourceTree = ""; }; 4CFF8F6229CC9AD7008DB934 /* ImageContextMenuModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageContextMenuModifier.swift; sourceTree = ""; }; 4CFF8F6629CC9E3A008DB934 /* ImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageView.swift; sourceTree = ""; }; 4CFF8F6829CC9ED1008DB934 /* ImageContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageContainerView.swift; sourceTree = ""; }; @@ -1249,6 +1257,9 @@ D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockProfiles.swift; sourceTree = ""; }; D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManager.swift; sourceTree = ""; }; D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManagerTests.swift; sourceTree = ""; }; + D74F43092B23F0BE00425B75 /* DamusPurple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurple.swift; sourceTree = ""; }; + D74F430B2B23FB9B00425B75 /* StoreObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreObserver.swift; sourceTree = ""; }; + D76556D52B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleWelcomeView.swift; sourceTree = ""; }; D76874F22AE3632B00FB0F68 /* ProfileZapLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileZapLinkView.swift; sourceTree = ""; }; D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileActionSheetView.swift; sourceTree = ""; }; D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedHashtagsView.swift; sourceTree = ""; }; @@ -1410,6 +1421,7 @@ 4C0A3F8D280F63FF000448DE /* Models */ = { isa = PBXGroup; children = ( + D74F43082B23F09300425B75 /* Purple */, BA3759882ABCCDE30018D73B /* Camera */, 4C190F1E2A535FC200027FD5 /* Zaps */, 4C54AA0829A55416003E4487 /* Notifications */, @@ -1799,6 +1811,7 @@ 4C1A9A2829DDF53B00516EAC /* Video */, 4C1A9A1B29DDCF8B00516EAC /* Settings */, 4CFF8F6129CC9A80008DB934 /* Images */, + 4CFF8F5729C9FD07008DB934 /* Purple */, 4CCEB7AC29B53D180078AA28 /* Search */, 4C30AC7029A5676F00E2BD5A /* Notifications */, 4CE0E2B029A3DF4700DB4CA2 /* Timeline */, @@ -2032,6 +2045,7 @@ isa = PBXGroup; children = ( 4C86F7C52A76C51100EC0817 /* AttachedWalletNotify.swift */, + 4C9D6D152B1AA9C6004E5CD9 /* DisplayTabBarNotify.swift */, 4C1253552A76C8C60004F4B8 /* BroadcastNotify.swift */, 4C1253512A76C6130004F4B8 /* ComposeNotify.swift */, 4CA352AD2A76C1AC003BB08B /* FollowedNotify.swift */, @@ -2241,6 +2255,7 @@ 4C32B9362A9AD44700DC3548 /* flatbuffers */, 4C9054862A6AEB4500811EEC /* nostrdb */, 4C19AE4A2A5CEF7C00C90DB7 /* nostrscript */, + 4C8AE1182A0320BE00B944E6 /* Purple.storekit */, 4C06670728FDE62900038D2A /* damus-c */, 4CE6DEE527F7A08100C66700 /* damus */, 4CE6DEF627F7A08200C66700 /* damusTests */, @@ -2421,6 +2436,15 @@ path = Posting; sourceTree = ""; }; + 4CFF8F5729C9FD07008DB934 /* Purple */ = { + isa = PBXGroup; + children = ( + 4CFF8F5829C9FD1E008DB934 /* DamusPurpleView.swift */, + D76556D52B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift */, + ); + path = Purple; + sourceTree = ""; + }; 4CFF8F6129CC9A80008DB934 /* Images */ = { isa = PBXGroup; children = ( @@ -2472,6 +2496,15 @@ path = Mocking; sourceTree = ""; }; + D74F43082B23F09300425B75 /* Purple */ = { + isa = PBXGroup; + children = ( + D74F43092B23F0BE00425B75 /* DamusPurple.swift */, + D74F430B2B23FB9B00425B75 /* StoreObserver.swift */, + ); + path = Purple; + sourceTree = ""; + }; D79C4C152AFEB061003A41B4 /* DamusNotificationService */ = { isa = PBXGroup; children = ( @@ -2875,6 +2908,7 @@ 3AAA95CA298DF87B00F3D526 /* TranslationService.swift in Sources */, 4CE4F9E328528C5200C00DD9 /* AddRelayView.swift in Sources */, BA3759922ABCCEBA0018D73B /* CameraService+Extensions.swift in Sources */, + D74F430C2B23FB9B00425B75 /* StoreObserver.swift in Sources */, 4C363A9A28283854006E126D /* Reply.swift in Sources */, BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */, 4CFF8F6729CC9E3A008DB934 /* ImageView.swift in Sources */, @@ -2970,9 +3004,11 @@ 4C3EA67928FF7ABF00C48A62 /* list.c in Sources */, 4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */, 4C12535E2A76CA870004F4B8 /* SwitchedTimelineNotify.swift in Sources */, + D74F430A2B23F0BE00425B75 /* DamusPurple.swift in Sources */, 9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */, 4C4E137D2A76D63600BDD832 /* UnmuteThreadNotify.swift in Sources */, 4CE4F0F829DB7399005914DB /* ThiccDivider.swift in Sources */, + 4CFF8F5929C9FD1E008DB934 /* DamusPurpleView.swift in Sources */, 4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */, 4C363A8828236948006E126D /* BlocksView.swift in Sources */, 4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */, @@ -3038,6 +3074,7 @@ 3A4647CF2A413ADC00386AD8 /* CondensedProfilePicturesView.swift in Sources */, D2277EEA2A089BD5006C3807 /* Router.swift in Sources */, 3A90B1812A4EA3AF00000D94 /* UserSearchCache.swift in Sources */, + 4C9D6D162B1AA9C6004E5CD9 /* DisplayTabBarNotify.swift in Sources */, 4CC14FF92A741939007AEB17 /* Referenced.swift in Sources */, 4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */, 4CE1399429F0669900AC6A0B /* BigButton.swift in Sources */, @@ -3054,6 +3091,7 @@ 4C7D096F2A0AEA0400943473 /* ScannerViewController.swift in Sources */, 4CEE2AF3280B25C500AB5EEF /* ProfilePicView.swift in Sources */, 4CC7AAF6297F1A6A00430951 /* EventBody.swift in Sources */, + D76556D62B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift in Sources */, 3165648B295B70D500C64604 /* LinkView.swift in Sources */, 4C8D00CF29E38B950036AF10 /* nostr_bech32.c in Sources */, 4C1253562A76C8C60004F4B8 /* BroadcastNotify.swift in Sources */, diff --git a/damus.xcodeproj/xcshareddata/xcschemes/damus.xcscheme b/damus.xcodeproj/xcshareddata/xcschemes/damus.xcscheme index b632202a..27579420 100644 --- a/damus.xcodeproj/xcshareddata/xcschemes/damus.xcscheme +++ b/damus.xcodeproj/xcshareddata/xcschemes/damus.xcscheme @@ -71,6 +71,9 @@ ReferencedContainer = "container:damus.xcodeproj"> + + \ No newline at end of file diff --git a/damus/Assets.xcassets/Purple/stars-bg.imageset/Contents.json b/damus/Assets.xcassets/Purple/stars-bg.imageset/Contents.json new file mode 100644 index 00000000..0650cb45 --- /dev/null +++ b/damus/Assets.xcassets/Purple/stars-bg.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "stars-bg.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/damus/Assets.xcassets/Purple/stars-bg.imageset/stars-bg.png b/damus/Assets.xcassets/Purple/stars-bg.imageset/stars-bg.png new file mode 100644 index 00000000..57ee8ece Binary files /dev/null and b/damus/Assets.xcassets/Purple/stars-bg.imageset/stars-bg.png differ diff --git a/damus/Assets.xcassets/gradient-backgrounds/Contents.json b/damus/Assets.xcassets/gradient-backgrounds/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/damus/Assets.xcassets/gradient-backgrounds/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/damus/Assets.xcassets/gradient-backgrounds/purple-blue-gradient-1.imageset/Contents.json b/damus/Assets.xcassets/gradient-backgrounds/purple-blue-gradient-1.imageset/Contents.json new file mode 100644 index 00000000..e5d3b3bc --- /dev/null +++ b/damus/Assets.xcassets/gradient-backgrounds/purple-blue-gradient-1.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "shadow-2.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/damus/Assets.xcassets/gradient-backgrounds/purple-blue-gradient-1.imageset/shadow-2.png b/damus/Assets.xcassets/gradient-backgrounds/purple-blue-gradient-1.imageset/shadow-2.png new file mode 100644 index 00000000..e8c4c788 Binary files /dev/null and b/damus/Assets.xcassets/gradient-backgrounds/purple-blue-gradient-1.imageset/shadow-2.png differ diff --git a/damus/Assets.xcassets/gradient-backgrounds/purple-gradient-1.imageset/Contents.json b/damus/Assets.xcassets/gradient-backgrounds/purple-gradient-1.imageset/Contents.json new file mode 100644 index 00000000..7c0193c7 --- /dev/null +++ b/damus/Assets.xcassets/gradient-backgrounds/purple-gradient-1.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "shadow.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/damus/Assets.xcassets/gradient-backgrounds/purple-gradient-1.imageset/shadow.png b/damus/Assets.xcassets/gradient-backgrounds/purple-gradient-1.imageset/shadow.png new file mode 100644 index 00000000..7cade7bd Binary files /dev/null and b/damus/Assets.xcassets/gradient-backgrounds/purple-gradient-1.imageset/shadow.png differ diff --git a/damus/Components/DamusColors.swift b/damus/Components/DamusColors.swift index 259e388c..50563d74 100644 --- a/damus/Components/DamusColors.swift +++ b/damus/Components/DamusColors.swift @@ -41,5 +41,8 @@ class DamusColors { static let neutral1 = Color("DamusNeutral1") static let neutral3 = Color("DamusNeutral3") static let neutral6 = Color("DamusNeutral6") + static let pink = Color(red: 211/255.0, green: 76/255.0, blue: 217/255.0) + static let lighterPink = Color(red: 248/255.0, green: 105/255.0, blue: 182/255.0) + static let lightBackgroundPink = Color(red: 0xF8/255.0, green: 0xE7/255.0, blue: 0xF8/255.0) } diff --git a/damus/Components/Search/SearchHeaderView.swift b/damus/Components/Search/SearchHeaderView.swift index 43afce37..d750f483 100644 --- a/damus/Components/Search/SearchHeaderView.swift +++ b/damus/Components/Search/SearchHeaderView.swift @@ -101,7 +101,7 @@ struct NonImageAvatar: View { var body: some View { ZStack { Circle() - .fill(Color(red: 0xF8/255.0, green: 0xE7/255.0, blue: 0xF8/255.0)) + .fill(DamusColors.lightBackgroundPink) .frame(width: 54, height: 54) content diff --git a/damus/ContentView.swift b/damus/ContentView.swift index 1bbf76aa..02b399e5 100644 --- a/damus/ContentView.swift +++ b/damus/ContentView.swift @@ -71,6 +71,7 @@ struct ContentView: View { @SceneStorage("ContentView.selected_timeline") var selected_timeline: Timeline = .home @State var muting: Pubkey? = nil @State var confirm_mute: Bool = false + @State var hide_bar: Bool = false @State var user_muted_confirm: Bool = false @State var confirm_overwrite_mutelist: Bool = false @SceneStorage("ContentView.filter_state") var filter_state : FilterState = .posts_and_replies @@ -284,12 +285,17 @@ struct ContentView: View { } .navigationViewStyle(.stack) - TabBar(nstatus: home.notification_status, selected: $selected_timeline, settings: damus.settings, action: switch_timeline) - .padding([.bottom], 8) - .background(Color(uiColor: .systemBackground).ignoresSafeArea()) + if !hide_bar { + TabBar(nstatus: home.notification_status, selected: $selected_timeline, settings: damus.settings, action: switch_timeline) + .padding([.bottom], 8) + .background(Color(uiColor: .systemBackground).ignoresSafeArea()) + } else { + Text("") + } } } .ignoresSafeArea(.keyboard) + .edgesIgnoringSafeArea(hide_bar ? [.bottom] : []) .onAppear() { self.connect() try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: .default, options: .mixWithOthers) @@ -344,6 +350,10 @@ struct ContentView: View { .onReceive(handle_notify(.compose)) { action in self.active_sheet = .post(action) } + .onReceive(handle_notify(.display_tabbar)) { display in + let show = display + self.hide_bar = !show + } .onReceive(timer) { n in self.damus_state?.postbox.try_flushing_events() self.damus_state!.profiles.profile_data(self.damus_state!.pubkey).status.try_expire() @@ -668,8 +678,17 @@ struct ContentView: View { video: VideoController(), ndb: ndb ) + home.damus_state = self.damus_state! + if let damus_state, damus_state.settings.enable_experimental_purple_api { + // Assign delegate so that we can send receipts to the Purple API server as soon as we get updates from user's purchases + StoreObserver.standard.delegate = damus_state.purple + } + else { + // Purple API is an experimental feature. If not enabled, do not connect `StoreObserver` with Purple API to avoid leaking receipts + } + pool.connect() } diff --git a/damus/Info.plist b/damus/Info.plist index 71c6af52..515123cc 100644 --- a/damus/Info.plist +++ b/damus/Info.plist @@ -2,6 +2,10 @@ + CFBundleDocumentTypes + + + CFBundleURLTypes diff --git a/damus/Models/DamusState.swift b/damus/Models/DamusState.swift index 108ee3f5..61fe394f 100644 --- a/damus/Models/DamusState.swift +++ b/damus/Models/DamusState.swift @@ -34,6 +34,39 @@ struct DamusState { let music: MusicController? let video: VideoController let ndb: Ndb + var purple: DamusPurple + + init(pool: RelayPool, keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, postbox: PostBox, bootstrap_relays: [String], replies: ReplyCounter, muted_threads: MutedThreadsManager, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: VideoController, ndb: Ndb, purple: DamusPurple? = nil) { + self.pool = pool + self.keypair = keypair + self.likes = likes + self.boosts = boosts + self.contacts = contacts + self.profiles = profiles + self.dms = dms + self.previews = previews + self.zaps = zaps + self.lnurls = lnurls + self.settings = settings + self.relay_filters = relay_filters + self.relay_model_cache = relay_model_cache + self.drafts = drafts + self.events = events + self.bookmarks = bookmarks + self.postbox = postbox + self.bootstrap_relays = bootstrap_relays + self.replies = replies + self.muted_threads = muted_threads + self.wallet = wallet + self.nav = nav + self.music = music + self.video = video + self.ndb = ndb + self.purple = purple ?? DamusPurple( + environment: settings.purple_api_local_test_mode ? .local_test : .production, + keypair: keypair + ) + } @discardableResult func add_zap(zap: Zapping) -> Bool { diff --git a/damus/Models/Purple/DamusPurple.swift b/damus/Models/Purple/DamusPurple.swift new file mode 100644 index 00000000..9b6692a5 --- /dev/null +++ b/damus/Models/Purple/DamusPurple.swift @@ -0,0 +1,134 @@ +// +// DamusPurple.swift +// damus +// +// Created by Daniel D’Aquino on 2023-12-08. +// + +import Foundation + +class DamusPurple: StoreObserverDelegate { + let environment: ServerEnvironment + let keypair: Keypair + var starred_profiles_cache: [Pubkey: Bool] + + init(environment: ServerEnvironment, keypair: Keypair) { + self.environment = environment + self.keypair = keypair + self.starred_profiles_cache = [:] + } + + // MARK: Functions + func is_profile_subscribed_to_purple(pubkey: Pubkey) async -> Bool? { + if let cached_result = self.starred_profiles_cache[pubkey] { + return cached_result + } + + guard let data = await self.get_account_data(pubkey: pubkey) else { return nil } + + if let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], + let active = json["active"] as? Bool { + self.starred_profiles_cache[pubkey] = active + return active + } + + return nil + } + + func account_exists(pubkey: Pubkey) async -> Bool? { + guard let account_data = await self.get_account_data(pubkey: pubkey) else { return nil } + + if let json = try? JSONSerialization.jsonObject(with: account_data, options: []) as? [String: Any], + let id = json["id"] as? String { + return id == pubkey.hex() + } + + return false + } + + func get_account_data(pubkey: Pubkey) async -> Data? { + let url = environment.get_base_url().appendingPathComponent("accounts/\(pubkey.hex())") + var request = URLRequest(url: url) + request.httpMethod = "GET" + + do { + let (data, _) = try await URLSession.shared.data(for: request) + return data + } catch { + print("Failed to fetch data: \(error)") + } + + return nil + } + + func create_account(pubkey: Pubkey) async throws { + let url = environment.get_base_url().appendingPathComponent("accounts") + var request = URLRequest(url: url) + request.httpMethod = "POST" + + let payload: [String: String] = [ + "pubkey": pubkey.hex() + ] + + request.httpBody = try JSONEncoder().encode(payload) + do { + let (_, _) = try await URLSession.shared.data(for: request) + return + } catch { + print("Failed to fetch data: \(error)") + } + + return + } + + func create_account_if_not_existing(pubkey: Pubkey) async throws { + guard await !(self.account_exists(pubkey: pubkey) ?? false) else { return } + try await self.create_account(pubkey: pubkey) + } + + func send_receipt() async { + // Get the receipt if it's available. + if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL, + FileManager.default.fileExists(atPath: appStoreReceiptURL.path) { + + try? await create_account_if_not_existing(pubkey: keypair.pubkey) + + do { + let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped) + print(receiptData) + + let url = environment.get_base_url().appendingPathComponent("accounts/\(keypair.pubkey.hex())/app-store-receipt") + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.httpBody = receiptData + + do { + let (_, _) = try await URLSession.shared.data(for: request) + print("Sent receipt") + } catch { + print("Failed to fetch data: \(error)") + } + + } + catch { print("Couldn't read receipt data with error: " + error.localizedDescription) } + } + } +} + +// MARK: Helper structures + +extension DamusPurple { + enum ServerEnvironment { + case local_test + case production + + func get_base_url() -> URL { + switch self { + case .local_test: + Constants.PURPLE_API_TEST_BASE_URL + case .production: + Constants.PURPLE_API_PRODUCTION_BASE_URL + } + } + } +} diff --git a/damus/Models/Purple/StoreObserver.swift b/damus/Models/Purple/StoreObserver.swift new file mode 100644 index 00000000..4a898b8f --- /dev/null +++ b/damus/Models/Purple/StoreObserver.swift @@ -0,0 +1,33 @@ +// +// StoreObserver.swift +// damus +// +// Created by Daniel D’Aquino on 2023-12-08. +// + +import Foundation +import StoreKit + +class StoreObserver: NSObject, SKPaymentTransactionObserver { + static let standard = StoreObserver() + + var delegate: StoreObserverDelegate? + + init(delegate: StoreObserverDelegate? = nil) { + self.delegate = delegate + super.init() + } + + //Observe transaction updates. + func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { + //Handle transaction states here. + + Task { + await self.delegate?.send_receipt() + } + } +} + +protocol StoreObserverDelegate { + func send_receipt() async +} diff --git a/damus/Models/UserSettingsStore.swift b/damus/Models/UserSettingsStore.swift index ea153878..74a00e14 100644 --- a/damus/Models/UserSettingsStore.swift +++ b/damus/Models/UserSettingsStore.swift @@ -201,6 +201,12 @@ class UserSettingsStore: ObservableObject { @Setting(key: "send_device_token_to_localhost", default_value: false) var send_device_token_to_localhost: Bool + @Setting(key: "enable_experimental_purple_api", default_value: false) + var enable_experimental_purple_api: Bool + + @Setting(key: "purple_api_local_test_mode", default_value: false) + var purple_api_local_test_mode: Bool + @Setting(key: "emoji_reactions", default_value: default_emoji_reactions) var emoji_reactions: [String] diff --git a/damus/Notify/DisplayTabBarNotify.swift b/damus/Notify/DisplayTabBarNotify.swift new file mode 100644 index 00000000..097860ee --- /dev/null +++ b/damus/Notify/DisplayTabBarNotify.swift @@ -0,0 +1,25 @@ +// +// DisplayTabBarNotify.swift +// damus +// +// Created by William Casarin on 2023-12-01. +// + +import Foundation + +struct DisplayTabBarNotify: Notify { + typealias Payload = Bool + var payload: Payload +} + +extension NotifyHandler { + static var display_tabbar: NotifyHandler { + .init() + } +} + +extension Notifications { + static func display_tabbar(_ payload: Bool) -> Notifications { + .init(.init(payload: payload)) + } +} diff --git a/damus/Util/Constants.swift b/damus/Util/Constants.swift index 6021090e..1eb5f376 100644 --- a/damus/Util/Constants.swift +++ b/damus/Util/Constants.swift @@ -8,5 +8,9 @@ import Foundation class Constants { + static let PURPLE_API_PRODUCTION_BASE_URL: URL = URL(string: "https://purple.damus.io")! + static let PURPLE_API_TEST_BASE_URL: URL = URL(string: "http://localhost:8989")! + static let DEVICE_TOKEN_RECEIVER_PRODUCTION_URL: URL = URL(string: "https://notify.damus.io:8000/user-info")! + static let DEVICE_TOKEN_RECEIVER_TEST_URL: URL = URL(string: "http://localhost:8000/user-info")! static let EXAMPLE_DEMOS: DamusState = .empty } diff --git a/damus/Views/LoginView.swift b/damus/Views/LoginView.swift index d264b904..756cf64f 100644 --- a/damus/Views/LoginView.swift +++ b/damus/Views/LoginView.swift @@ -87,7 +87,6 @@ struct LoginView: View { } if let p = parsed { - Button(action: { Task { do { diff --git a/damus/Views/NoteContentView.swift b/damus/Views/NoteContentView.swift index 049528b7..eacd8d1b 100644 --- a/damus/Views/NoteContentView.swift +++ b/damus/Views/NoteContentView.swift @@ -208,6 +208,10 @@ struct NoteContentView: View { } func load(force_artifacts: Bool = false) { + if case .loading = damus_state.events.get_cache_data(event.id).artifacts_model.state { + return + } + // always reload artifacts on load let plan = get_preload_plan(evcache: damus_state.events, ev: event, our_keypair: damus_state.keypair, settings: damus_state.settings) diff --git a/damus/Views/Profile/EventProfileName.swift b/damus/Views/Profile/EventProfileName.swift index 28cec949..19435e22 100644 --- a/damus/Views/Profile/EventProfileName.swift +++ b/damus/Views/Profile/EventProfileName.swift @@ -10,12 +10,13 @@ import SwiftUI /// Profile Name used when displaying an event in the timeline @MainActor struct EventProfileName: View { - let damus_state: DamusState + var damus_state: DamusState let pubkey: Pubkey @State var display_name: DisplayName? @State var nip05: NIP05? @State var donation: Int? + @State var is_purple_user: Bool? let size: EventViewKind @@ -25,6 +26,7 @@ struct EventProfileName: View { self.size = size let donation = damus.ndb.lookup_profile(pubkey).map({ p in p?.profile?.damus_donation }).value self._donation = State(wrappedValue: donation) + is_purple_user = nil } var friend_type: FriendType? { @@ -47,7 +49,12 @@ struct EventProfileName: View { return profile.reactions == false } - var supporter: Int? { + func supporter_percentage() -> Int? { + if damus_state.settings.enable_experimental_purple_api, + is_purple_user == true { + return 100 + } + guard let donation, donation > 0 else { return nil @@ -92,7 +99,7 @@ struct EventProfileName: View { .frame(width: 14, height: 14) } - if let supporter { + if let supporter = self.supporter_percentage() { SupporterBadge(percent: supporter) } } @@ -119,6 +126,13 @@ struct EventProfileName: View { donation = profile.damus_donation } } + .onAppear(perform: { + Task { + if damus_state.settings.enable_experimental_purple_api { + is_purple_user = await damus_state.purple.is_profile_subscribed_to_purple(pubkey: self.pubkey) ?? false + } + } + }) } } diff --git a/damus/Views/Purple/DamusPurpleView.swift b/damus/Views/Purple/DamusPurpleView.swift new file mode 100644 index 00000000..c524aaff --- /dev/null +++ b/damus/Views/Purple/DamusPurpleView.swift @@ -0,0 +1,428 @@ +// +// DamusPurpleView.swift +// damus +// +// Created by William Casarin on 2023-03-21. +// + +import SwiftUI +import StoreKit + +fileprivate let damus_products = ["purpleyearly","purple"] + +enum ProductState { + case loading + case loaded([Product]) + case failed + + var products: [Product]? { + switch self { + case .loading: + return nil + case .loaded(let ps): + return ps + case .failed: + return nil + } + } +} + +func non_discounted_price(_ product: Product) -> String { + return (product.price * 1.1984569224).formatted(product.priceFormatStyle) +} + +enum DamusPurpleType: String { + case yearly = "purpleyearly" + case monthly = "purple" +} + +struct PurchasedProduct { + let tx: StoreKit.Transaction + let product: Product +} + +struct DamusPurpleView: View { + let purple_api: DamusPurple + let keypair: Keypair + + @State var products: ProductState + @State var purchased: PurchasedProduct? = nil + @State var selection: DamusPurpleType = .yearly + @State var show_welcome_sheet: Bool = false + @State var show_manage_subscriptions = false + + @Environment(\.dismiss) var dismiss + + init(purple: DamusPurple, keypair: Keypair) { + self._products = State(wrappedValue: .loading) + self.purple_api = purple + self.keypair = keypair + } + + var body: some View { + ZStack { + Rectangle() + .background(.black) + + ScrollView { + MainContent + .padding(.top, 75) + .background(content: { + ZStack { + Image("purple-blue-gradient-1") + .offset(CGSize(width: 300.0, height: -0.0)) + Image("purple-blue-gradient-1") + .offset(CGSize(width: 300.0, height: -0.0)) + + } + }) + } + } + .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(true) + .navigationBarItems(leading: BackNav()) + .onReceive(handle_notify(.switched_timeline)) { _ in + dismiss() + } + .onAppear { + notify(.display_tabbar(false)) + } + .onDisappear { + notify(.display_tabbar(true)) + } + .task { + await load_products() + } + .ignoresSafeArea(.all) + .sheet(isPresented: $show_welcome_sheet, content: { + DamusPurpleWelcomeView() + }) + .manageSubscriptionsSheet(isPresented: $show_manage_subscriptions) + } + + func handle_transactions(products: [Product]) async { + for await update in StoreKit.Transaction.updates { + switch update { + case .verified(let tx): + let prod = products.filter({ prod in tx.productID == prod.id }).first + + if let prod, + let expiration = tx.expirationDate, + Date.now < expiration + { + self.purchased = PurchasedProduct(tx: tx, product: prod) + break + } + case .unverified: + continue + } + } + } + + func load_products() async { + do { + let products = try await Product.products(for: damus_products) + self.products = .loaded(products) + await handle_transactions(products: products) + + print("loaded products", products) + } catch { + self.products = .failed + print("Failed to fetch products: \(error.localizedDescription)") + } + } + + func IconOnBox(_ name: String) -> some View { + ZStack { + RoundedRectangle(cornerRadius: 20.0) + .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 20.0)) + .frame(width: 80, height: 80) + .overlay( + RoundedRectangle(cornerRadius: 20) + .stroke(LinearGradient( + colors: [DamusColors.pink, .white.opacity(0), .white.opacity(0.5), .white.opacity(0)], + startPoint: .topLeading, + endPoint: .bottomTrailing), lineWidth: 1) + ) + + Image(name) + .resizable() + .frame(width: 50, height: 50) + .foregroundColor(.white) + } + } + + func Icon(_ name: String) -> some View { + Image(name) + .resizable() + .frame(width: 50, height: 50) + .foregroundColor(.white) + } + + func Title(_ txt: String) -> some View { + Text(txt) + .font(.title3) + .bold() + .foregroundColor(.white) + .padding(.bottom, 3) + } + + func Subtitle(_ txt: String) -> some View { + Text(txt) + .foregroundColor(.white.opacity(0.65)) + } + + var ProductLoadError: some View { + Text("Ah dang there was an error loading subscription information from the AppStore. Please try again later :(") + .foregroundColor(.white) + } + + var SaveText: Text { + Text("Save 14%") + .font(.callout) + .italic() + .foregroundColor(DamusColors.green) + } + + func subscribe(_ product: Product) async throws { + let result = try await product.purchase() + switch result { + case .success(.verified(let tx)): + print("success \(tx.debugDescription)") + show_welcome_sheet = true + case .success(.unverified(let tx, let res)): + print("success unverified \(tx.debugDescription) \(res.localizedDescription)") + show_welcome_sheet = true + case .pending: + break + case .userCancelled: + break + @unknown default: + break + } + + switch result { + case .success: + self.purple_api.starred_profiles_cache[keypair.pubkey] = nil + Task { + await self.purple_api.send_receipt() + } + default: + break + } + } + + var product: Product? { + return self.products.products?.filter({ + prod in prod.id == selection.rawValue + }).first + } + + func price_description(product: Product) -> some View { + if product.id == "purpleyearly" { + return ( + AnyView( + HStack(spacing: 10) { + Text("Anually") + Spacer() + Text(verbatim: non_discounted_price(product)).strikethrough().foregroundColor(DamusColors.white.opacity(0.5)) + Text(verbatim: product.displayPrice).fontWeight(.bold) + } + ) + ) + } else { + return ( + AnyView( + HStack(spacing: 10) { + Text("Monthly") + Spacer() + Text(verbatim: product.displayPrice).fontWeight(.bold) + } + ) + ) + } + } + + func ProductsView(_ products: [Product]) -> some View { + VStack(spacing: 10) { + Text("Save 20% off on an annual subscription") + .font(.callout.bold()) + .foregroundColor(.white) + ForEach(products) { product in + Button(action: { + Task { @MainActor in + do { + try await subscribe(product) + } catch { + print(error.localizedDescription) + } + } + }, label: { + price_description(product: product) + }) + .buttonStyle(GradientButtonStyle()) + } + } + .padding(.horizontal, 20) + } + + func PurchasedView(_ purchased: PurchasedProduct) -> some View { + VStack(spacing: 10) { + Text("Purchased!") + .font(.title2) + .foregroundColor(.white) + price_description(product: purchased.product) + .foregroundColor(.white) + .opacity(0.65) + .frame(width: 200) + Text("Purchased on") + .font(.title2) + .foregroundColor(.white) + Text(format_date(UInt32(purchased.tx.purchaseDate.timeIntervalSince1970))) + .foregroundColor(.white) + .opacity(0.65) + if let expiry = purchased.tx.expirationDate { + Text("Renews on") + .font(.title2) + .foregroundColor(.white) + Text(format_date(UInt32(expiry.timeIntervalSince1970))) + .foregroundColor(.white) + .opacity(0.65) + } + Button(action: { + show_manage_subscriptions = true + }, label: { + Text("Manage") + }) + .buttonStyle(GradientButtonStyle()) + } + } + + var ProductStateView: some View { + Group { + switch self.products { + case .failed: + ProductLoadError + case .loaded(let products): + if let purchased { + PurchasedView(purchased) + } else { + ProductsView(products) + } + case .loading: + ProgressView() + .progressViewStyle(.circular) + } + } + } + + var MainContent: some View { + VStack { + HStack(spacing: 20) { + Image("damus-dark-logo") + .resizable() + .frame(width: 60, height: 60) + .clipShape(RoundedRectangle(cornerRadius: 15.0)) + .overlay( + RoundedRectangle(cornerRadius: 15) + .stroke(LinearGradient( + colors: [DamusColors.lighterPink.opacity(0.8), .white.opacity(0), DamusColors.deepPurple.opacity(0.6)], + startPoint: .topLeading, + endPoint: .bottomTrailing), lineWidth: 1) + ) + .shadow(radius: 5) + + VStack(alignment: .leading) { + Text("Purple") + .font(.system(size: 60.0).weight(.bold)) + .foregroundStyle( + LinearGradient( + colors: [DamusColors.lighterPink, DamusColors.deepPurple], + startPoint: .bottomLeading, + endPoint: .topTrailing + ) + ) + .foregroundColor(.white) + .tracking(-2) + } + } + .padding(.bottom, 30) + + VStack(alignment: .leading, spacing: 30) { + Subtitle("Help us stay independent in our mission for Freedom tech with our Purple subscription, and look cool doing it!") + .multilineTextAlignment(.center) + + HStack(spacing: 20) { + IconOnBox("heart.fill") + + VStack(alignment: .leading) { + Title("Help Build The Future") + + Subtitle("Support Damus development to help build the future of decentralized communication on the web.") + } + } + + HStack(spacing: 20) { + IconOnBox("ai-3-stars.fill") + + VStack(alignment: .leading) { + Title("Exclusive features") + .padding(.bottom, -3) + + HStack(spacing: 3) { + Image("calendar") + .resizable() + .frame(width: 15, height: 15) + + Text("Coming soon") + .font(.caption) + .bold() + } + .foregroundColor(DamusColors.pink) + .padding(.vertical, 3) + .padding(.horizontal, 8) + .background(DamusColors.lightBackgroundPink) + .cornerRadius(30.0) + + Subtitle("Be the first to access upcoming premium features: Automatic translations, longer note storage, and more") + .padding(.top, 3) + } + } + + HStack(spacing: 20) { + IconOnBox("badge") + + VStack(alignment: .leading) { + Title("Supporter Badge") + + Subtitle("Get a special badge on your profile to show everyone your contribution to Freedom tech") + } + } + + } + .padding([.trailing, .leading], 30) + .padding(.bottom, 20) + + VStack(alignment: .center) { + ProductStateView + } + .padding([.top], 20) + + + Spacer() + } + } +} + +struct DamusPurpleView_Previews: PreviewProvider { + static var previews: some View { + /* + DamusPurpleView(products: [ + DamusProduct(name: "Yearly", id: "purpleyearly", price: Decimal(69.99)), + DamusProduct(name: "Monthly", id: "purple", price: Decimal(6.99)), + ]) + */ + + DamusPurpleView(purple: test_damus_state.purple, keypair: test_damus_state.keypair) + } +} diff --git a/damus/Views/Purple/DamusPurpleWelcomeView.swift b/damus/Views/Purple/DamusPurpleWelcomeView.swift new file mode 100644 index 00000000..7b7a1a23 --- /dev/null +++ b/damus/Views/Purple/DamusPurpleWelcomeView.swift @@ -0,0 +1,127 @@ +// +// DamusPurpleWelcomeView.swift +// damus +// +// Created by Daniel D’Aquino on 2023-12-04. +// + +import Foundation +import SwiftUI + +fileprivate extension Animation { + static func content() -> Animation { + Animation.easeInOut(duration: 1).delay(3) + } +} + +struct DamusPurpleWelcomeView: View { + @Environment(\.dismiss) var dismiss + @State var start = false + + var body: some View { + VStack { + Image("damus-dark-logo") + .resizable() + .frame(width: 50, height: 50) + .clipShape(RoundedRectangle(cornerRadius: 10.0)) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(LinearGradient( + colors: [DamusColors.lighterPink.opacity(0.8), .white.opacity(0), DamusColors.deepPurple.opacity(0.6)], + startPoint: .topLeading, + endPoint: .bottomTrailing), lineWidth: 1) + ) + .shadow(radius: 5) + .padding(20) + .opacity(start ? 1.0 : 0.0) + .animation(.content(), value: start) + + Text("Welcome to Purple") + .font(.largeTitle) + .fontWeight(.bold) + .foregroundStyle( + LinearGradient( + colors: [.black, .black, DamusColors.pink, DamusColors.lighterPink], + startPoint: start ? .init(x: -3, y: 4) : .bottomLeading, + endPoint: start ? .topTrailing : .init(x: 3, y: -4) + ) + ) + .opacity(start ? 1.0 : 0.0) + .animation(Animation.easeInOut(duration: 3).delay(0), value: start) + + Image(systemName: "star.fill") + .resizable() + .frame(width: 96, height: 90) + .foregroundStyle( + LinearGradient( + colors: [.black, DamusColors.purple, .white, .white], + startPoint: start ? .init(x: -1, y: 1.5) : .bottomLeading, + endPoint: start ? .topTrailing : .init(x: 10, y: -11) + ) + ) + .animation(Animation.snappy(duration: 3).delay(1), value: start) + .shadow( + color: start ? DamusColors.lightBackgroundPink : DamusColors.purple.opacity(0.3), + radius: start ? 30 : 10 + ) + .animation(Animation.snappy(duration: 3).delay(0), value: start) + .scaleEffect(x: start ? 1 : 3, y: start ? 1 : 3) + .opacity(start ? 1.0 : 0.0) + .animation(Animation.snappy(duration: 2).delay(0), value: start) + + Text("Thank you very much for signing up for Damus\u{00A0}Purple. Your contribution helps us continue our fight for a more Open and Free\u{00A0}internet.\n\nYou will also get access to premium features, and a star badge on your profile.\n\nEnjoy!") + .lineSpacing(5) + .multilineTextAlignment(.center) + .foregroundStyle(.white.opacity(0.8)) + .padding(.horizontal, 20) + .padding(.top, 50) + .padding(.bottom, 20) + .opacity(start ? 1.0 : 0.0) + .animation(.content(), value: start) + + Button(action: { + dismiss() + }, label: { + HStack { + Spacer() + Text("Continue") + Spacer() + } + }) + .padding(.horizontal, 30) + .buttonStyle(GradientButtonStyle()) + .opacity(start ? 1.0 : 0.0) + .animation(Animation.easeInOut(duration: 2).delay(5), value: start) + } + .background(content: { + ZStack { + Rectangle() + .background(.black) + Image("purple-blue-gradient-1") + .offset(CGSize(width: 300.0, height: -0.0)) + .opacity(start ? 1.0 : 0.2) + Image("stars-bg") + .resizable(resizingMode: .stretch) + .frame(width: 500, height: 500) + .offset(x: -100, y: 50) + .scaleEffect(start ? 1 : 1.1) + .animation(Animation.easeOut(duration: 3).delay(0), value: start) + Image("purple-blue-gradient-1") + .offset(CGSize(width: 300.0, height: -0.0)) + .opacity(start ? 1.0 : 0.2) + + } + }) + .onAppear(perform: { + withAnimation(.easeOut(duration: 6), { + start = true + }) + }) + } +} + +struct DamusPurpleWelcomeView_Previews: PreviewProvider { + static var previews: some View { + DamusPurpleWelcomeView() + } +} diff --git a/damus/Views/Settings/DeveloperSettingsView.swift b/damus/Views/Settings/DeveloperSettingsView.swift index 110fa3e4..3ceb322b 100644 --- a/damus/Views/Settings/DeveloperSettingsView.swift +++ b/damus/Views/Settings/DeveloperSettingsView.swift @@ -24,6 +24,12 @@ struct DeveloperSettingsView: View { Toggle(NSLocalizedString("Send device token to localhost", comment: "Developer mode setting to send device token metadata to a local server instead of the damus.io server."), isOn: $settings.send_device_token_to_localhost) .toggleStyle(.switch) + + Toggle("Enable experimental Purple API support", isOn: $settings.enable_experimental_purple_api) + .toggleStyle(.switch) + + Toggle("Purple API localhost test mode", isOn: $settings.purple_api_local_test_mode) + .toggleStyle(.switch) } } } diff --git a/damus/Views/SideMenuView.swift b/damus/Views/SideMenuView.swift index 24f43027..b47b0c83 100644 --- a/damus/Views/SideMenuView.swift +++ b/damus/Views/SideMenuView.swift @@ -39,6 +39,7 @@ struct SideMenuView: View { .onTapGesture { isSidebarVisible.toggle() } + content } } @@ -51,17 +52,18 @@ struct SideMenuView: View { NavigationLink(value: Route.Wallet(wallet: damus_state.wallet)) { navLabel(title: NSLocalizedString("Wallet", comment: "Sidebar menu label for Wallet view."), img: "wallet") - /* - HStack { - Image("wallet") - .tint(DamusColors.adaptableBlack) + } - Text(NSLocalizedString("wallet", comment: "Sidebar menu label for Wallet view.")) - .font(.title2) - .foregroundColor(textColor()) - .frame(maxWidth: .infinity, alignment: .leading) - .dynamicTypeSize(.xSmall) - }*/ + if damus_state.settings.enable_experimental_purple_api { + NavigationLink(destination: DamusPurpleView(purple: damus_state.purple, keypair: damus_state.keypair)) { + HStack(spacing: 13) { + Image("nostr-hashtag") + Text("Purple") + .foregroundColor(DamusColors.purple) + .font(.title2.weight(.bold)) + } + .frame(maxWidth: .infinity, alignment: .leading) + } } NavigationLink(value: Route.MuteList(users: get_mutelist_users(damus_state.contacts.mutelist))) { diff --git a/damus/damusApp.swift b/damus/damusApp.swift index d32d57fc..5c96d048 100644 --- a/damus/damusApp.swift +++ b/damus/damusApp.swift @@ -6,6 +6,7 @@ // import SwiftUI +import StoreKit @main struct damusApp: App { @@ -61,6 +62,8 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { UNUserNotificationCenter.current().delegate = self + + SKPaymentQueue.default().add(StoreObserver.standard) return true } @@ -84,7 +87,7 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele let json: [String: Any] = ["deviceToken": token, "pubkey": pubkey.hex()] // create post request - let url = URL(string: settings.send_device_token_to_localhost ? "http://localhost:8000/user-info" : "https://notify.damus.io:8000/user-info")! + let url = settings.send_device_token_to_localhost ? Constants.DEVICE_TOKEN_RECEIVER_TEST_URL : Constants.DEVICE_TOKEN_RECEIVER_PRODUCTION_URL var request = URLRequest(url: url) request.httpMethod = "POST"