damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

commit 4703ed80a7c27fbca5704b7e633cff06ff0141dd
parent f7e407e030cfe34ae726d8c2509307bebc6b0c9a
Author: Daniel D’Aquino <daniel@daquino.me>
Date:   Wed, 22 Mar 2023 07:24:34 -0600

Damus Purple initial Proof-of-Concept support

This commit includes various code changes necessary to get a basic proof of concept of the feature working.

This is NOT a full working feature yet, only a preliminary prototype/PoC. It includes:
- [X] Basic Storekit configuration
- [X] Basic purchase mechanism
- [X] Basic layout and copywriting
- [X] Basic design
- [X] Manage button (To help user cancel their subscription)
- [X] Thank you confirmation + special welcome view
- [X] Star badge on profile (by checking the Damus Purple API)
- [X] Connection to Damus purple API for fetching account info, registering for an account and sending over the App Store receipt data

The feature sits behind a feature flag which is OFF by default (it can be turned ON via Settings --> Developer settings --> Enable experimental Purple API and restarting the app)

Testing
---------

PASS

Device: iPhone 15 Pro simulator
iOS: 17.0.1
Damus: This commit
damus-api: 59ce44a92cff1c1aaed9886f9befbd5f1053821d
Server: Ubuntu 22.04 (VM)
Setup:
1. On the server, delete the `mdb` database files to start from scratch
2. In iOS, reinstall the app if necessary to make sure there are no in-app purchases
3. Enable subscriptions support via developer settings with localhost test mode and restart app
4. Start server with mock parameters (Run `npm run dev`)

Steps:
1. Open top bar and click on "Purple"
2. Purple screen should appear and show both benefits and the purchase options. PASS
3. Click on "monthly". An Apple screen to confirm purchase should appear. PASS
4. Welcome screen with animation should appear. PASS
5. Click continue and restart app (Due to known issue tracked at https://github.com/damus-io/damus/issues/1814)
6. Post something
7. Gold star should appear beside your name
8. Look at the server logs. There should be some requests to create the account (POST), to send the receipt (POST), and to get account status
9. Go to purple view. There should be some information about the subscription, as well as a "manage" button. PASS
10. Click on "manage" button. An iOS sheet should appear allow the user to unsubscribe or manage their subscription to Damus Purple.

Feature flag testing
--------------------

PASS

Preconditions: Continue from above test
Steps:
1. Disable Damus Purple experiment support on developer settings. Restart the app.
2. Check your post. There should be no star beside your profile name. PASS
3. Check side menu. There should be no "Damus Purple" option. PASS
4. Check server logs. There should be no new requests being done to the server. PASS

Closes: https://github.com/damus-io/damus/issues/1422

Diffstat:
APurple.storekit | 125+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus.xcodeproj/project.pbxproj | 38++++++++++++++++++++++++++++++++++++++
Mdamus.xcodeproj/xcshareddata/xcschemes/damus.xcscheme | 3+++
Adamus/Assets.xcassets/Purple/Contents.json | 6++++++
Adamus/Assets.xcassets/Purple/damus-dark-gray-logo.imageset/Contents.json | 21+++++++++++++++++++++
Adamus/Assets.xcassets/Purple/damus-dark-gray-logo.imageset/Damus dark-gray.png | 0
Adamus/Assets.xcassets/Purple/damus-dark-logo.imageset/Contents.json | 21+++++++++++++++++++++
Adamus/Assets.xcassets/Purple/damus-dark-logo.imageset/Damus dark.png | 0
Adamus/Assets.xcassets/Purple/special-features.imageset/Contents.json | 23+++++++++++++++++++++++
Adamus/Assets.xcassets/Purple/special-features.imageset/special-features.svg | 2++
Adamus/Assets.xcassets/Purple/stars-bg.imageset/Contents.json | 21+++++++++++++++++++++
Adamus/Assets.xcassets/Purple/stars-bg.imageset/stars-bg.png | 0
Adamus/Assets.xcassets/gradient-backgrounds/Contents.json | 6++++++
Adamus/Assets.xcassets/gradient-backgrounds/purple-blue-gradient-1.imageset/Contents.json | 21+++++++++++++++++++++
Adamus/Assets.xcassets/gradient-backgrounds/purple-blue-gradient-1.imageset/shadow-2.png | 0
Adamus/Assets.xcassets/gradient-backgrounds/purple-gradient-1.imageset/Contents.json | 21+++++++++++++++++++++
Adamus/Assets.xcassets/gradient-backgrounds/purple-gradient-1.imageset/shadow.png | 0
Mdamus/Components/DamusColors.swift | 3+++
Mdamus/Components/Search/SearchHeaderView.swift | 2+-
Mdamus/ContentView.swift | 25++++++++++++++++++++++---
Mdamus/Info.plist | 4++++
Mdamus/Models/DamusState.swift | 33+++++++++++++++++++++++++++++++++
Adamus/Models/Purple/DamusPurple.swift | 134+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adamus/Models/Purple/StoreObserver.swift | 33+++++++++++++++++++++++++++++++++
Mdamus/Models/UserSettingsStore.swift | 6++++++
Adamus/Notify/DisplayTabBarNotify.swift | 25+++++++++++++++++++++++++
Mdamus/Util/Constants.swift | 4++++
Mdamus/Views/LoginView.swift | 1-
Mdamus/Views/NoteContentView.swift | 4++++
Mdamus/Views/Profile/EventProfileName.swift | 20+++++++++++++++++---
Adamus/Views/Purple/DamusPurpleView.swift | 428+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adamus/Views/Purple/DamusPurpleWelcomeView.swift | 127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus/Views/Settings/DeveloperSettingsView.swift | 6++++++
Mdamus/Views/SideMenuView.swift | 24+++++++++++++-----------
Mdamus/damusApp.swift | 5++++-
35 files changed, 1172 insertions(+), 20 deletions(-)

diff --git a/Purple.storekit 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 @@ -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 = "<group>"; }; 4C86F7C32A76C44C00EC0817 /* ZappingNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZappingNotify.swift; sourceTree = "<group>"; }; 4C86F7C52A76C51100EC0817 /* AttachedWalletNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachedWalletNotify.swift; sourceTree = "<group>"; }; + 4C8AE1182A0320BE00B944E6 /* Purple.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = Purple.storekit; sourceTree = "<group>"; }; 4C8D00C729DF791C0036AF10 /* CompatibleAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompatibleAttribute.swift; sourceTree = "<group>"; }; 4C8D00C929DF80350036AF10 /* TruncatedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TruncatedText.swift; sourceTree = "<group>"; }; 4C8D00CB29DF92DF0036AF10 /* Hashtags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hashtags.swift; sourceTree = "<group>"; }; @@ -1059,6 +1065,7 @@ 4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayName.swift; sourceTree = "<group>"; }; 4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventProfileName.swift; sourceTree = "<group>"; }; 4C9D6D1A2B1D35D7004E5CD9 /* PullDownSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullDownSearch.swift; sourceTree = "<group>"; }; + 4C9D6D152B1AA9C6004E5CD9 /* DisplayTabBarNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayTabBarNotify.swift; sourceTree = "<group>"; }; 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeZapView.swift; sourceTree = "<group>"; }; 4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaybeAnonPfpView.swift; sourceTree = "<group>"; }; 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = "<group>"; }; @@ -1184,6 +1191,7 @@ 4CF0ABF52985CD5500D66079 /* UserSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSearch.swift; sourceTree = "<group>"; }; 4CF38C872A9442DC00BE01B6 /* UserStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserStatusView.swift; sourceTree = "<group>"; }; 4CFD502E2A2DA45800A229DB /* MediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaView.swift; sourceTree = "<group>"; }; + 4CFF8F5829C9FD1E008DB934 /* DamusPurpleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleView.swift; sourceTree = "<group>"; }; 4CFF8F6229CC9AD7008DB934 /* ImageContextMenuModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageContextMenuModifier.swift; sourceTree = "<group>"; }; 4CFF8F6629CC9E3A008DB934 /* ImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageView.swift; sourceTree = "<group>"; }; 4CFF8F6829CC9ED1008DB934 /* ImageContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageContainerView.swift; sourceTree = "<group>"; }; @@ -1249,6 +1257,9 @@ D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockProfiles.swift; sourceTree = "<group>"; }; D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManager.swift; sourceTree = "<group>"; }; D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManagerTests.swift; sourceTree = "<group>"; }; + D74F43092B23F0BE00425B75 /* DamusPurple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurple.swift; sourceTree = "<group>"; }; + D74F430B2B23FB9B00425B75 /* StoreObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreObserver.swift; sourceTree = "<group>"; }; + D76556D52B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleWelcomeView.swift; sourceTree = "<group>"; }; D76874F22AE3632B00FB0F68 /* ProfileZapLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileZapLinkView.swift; sourceTree = "<group>"; }; D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileActionSheetView.swift; sourceTree = "<group>"; }; D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedHashtagsView.swift; sourceTree = "<group>"; }; @@ -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 = "<group>"; }; + 4CFF8F5729C9FD07008DB934 /* Purple */ = { + isa = PBXGroup; + children = ( + 4CFF8F5829C9FD1E008DB934 /* DamusPurpleView.swift */, + D76556D52B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift */, + ); + path = Purple; + sourceTree = "<group>"; + }; 4CFF8F6129CC9A80008DB934 /* Images */ = { isa = PBXGroup; children = ( @@ -2472,6 +2496,15 @@ path = Mocking; sourceTree = "<group>"; }; + D74F43082B23F09300425B75 /* Purple */ = { + isa = PBXGroup; + children = ( + D74F43092B23F0BE00425B75 /* DamusPurple.swift */, + D74F430B2B23FB9B00425B75 /* StoreObserver.swift */, + ); + path = Purple; + sourceTree = "<group>"; + }; 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 @@ -71,6 +71,9 @@ ReferencedContainer = "container:damus.xcodeproj"> </BuildableReference> </BuildableProductRunnable> + <StoreKitConfigurationFileReference + identifier = "../../Purple.storekit"> + </StoreKitConfigurationFileReference> </LaunchAction> <ProfileAction buildConfiguration = "Release" diff --git a/damus/Assets.xcassets/Purple/Contents.json b/damus/Assets.xcassets/Purple/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/damus/Assets.xcassets/Purple/damus-dark-gray-logo.imageset/Contents.json b/damus/Assets.xcassets/Purple/damus-dark-gray-logo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Damus dark-gray.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/damus-dark-gray-logo.imageset/Damus dark-gray.png b/damus/Assets.xcassets/Purple/damus-dark-gray-logo.imageset/Damus dark-gray.png Binary files differ. diff --git a/damus/Assets.xcassets/Purple/damus-dark-logo.imageset/Contents.json b/damus/Assets.xcassets/Purple/damus-dark-logo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Damus dark.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/damus-dark-logo.imageset/Damus dark.png b/damus/Assets.xcassets/Purple/damus-dark-logo.imageset/Damus dark.png Binary files differ. diff --git a/damus/Assets.xcassets/Purple/special-features.imageset/Contents.json b/damus/Assets.xcassets/Purple/special-features.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "special-features.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "special-features.svg", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "special-features.svg", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/damus/Assets.xcassets/Purple/special-features.imageset/special-features.svg b/damus/Assets.xcassets/Purple/special-features.imageset/special-features.svg @@ -0,0 +1 @@ +<svg id="_x33_0" enable-background="new 0 0 62 62" height="512" viewBox="0 0 62 62" width="512" xmlns="http://www.w3.org/2000/svg"><g><g><g><path d="m55.73 59.91.27 1.09h-8l.27-1.09c.43-1.71 1.97-2.91 3.73-2.91.88 0 1.7.3 2.36.81.66.52 1.16 1.25 1.37 2.1z" fill="#ff826e"/></g><g><path d="m55 53.07v.93c0 1.66-1.34 3-3 3-.83 0-1.58-.34-2.12-.88s-.88-1.29-.88-2.12v-1.22c0-.98.8-1.78 1.78-1.78.15 0 .29.02.43.05l2.75.69c.61.15 1.04.7 1.04 1.33z" fill="#f0d0b4"/></g><g><path d="m55.96 53.04-.96.96v-.93c0-.63-.43-1.18-1.04-1.33l-2.75-.69c-.14-.03-.28-.05-.43-.05-.98 0-1.78.8-1.78 1.78v1.22l-.96-.96c-.67-.67-1.04-1.57-1.04-2.5 0-.98.4-1.86 1.04-2.5s1.52-1.04 2.5-1.04h2.92c1.96 0 3.54 1.58 3.54 3.54 0 .93-.37 1.83-1.04 2.5z" fill="#656d78"/></g><g><path d="m55 15v1c0 .55-.45 1-1 1h-8c-.55 0-1-.45-1-1v-8c0-.55.45-1 1-1h8c.55 0 1 .45 1 1v1z" fill="#aab2bd"/></g><g><path d="m61 7v10l-6-2v-6z" fill="#ccd1d9"/></g><g><path d="m60.38 38.62c.38.38.62.92.62 1.5 0 .8-.32 1.52-.84 2.04s-1.24.84-2.04.84c-3.93 0-7.12-3.19-7.12-7.12v-1.35c0-1.95 1.58-3.53 3.53-3.53.68 0 1.3.28 1.75.72.44.45.72 1.07.72 1.75 0 .94-.53 1.79-1.37 2.21l-.63.32v1c0 .55.22 1.05.59 1.41.36.37.86.59 1.41.59l.38-.38c.4-.4.94-.62 1.5-.62.58 0 1.12.24 1.5.62z" fill="#ccd1d9"/></g><g><path d="m31 1c4.42 0 8 3.58 8 8s-3.58 8-8 8-8-3.58-8-8 3.58-8 8-8z" fill="#e6e9ed"/></g><g><path d="m40.85 26.15c1.36 1.94 2.15 4.3 2.15 6.85 0 4.08-2.04 7.69-5.15 9.85-1.94 1.36-4.3 2.15-6.85 2.15s-4.91-.79-6.85-2.15c-3.11-2.16-5.15-5.77-5.15-9.85 0-2.55.79-4.91 2.15-6.85 2.16-3.11 5.77-5.15 9.85-5.15s7.69 2.04 9.85 5.15z" fill="#69d6f4"/></g><g><path d="m17 8v6c0 .55-.45 1-1 1h-5l-.76 3.03c-.14.57-.65.97-1.24.97s-1.1-.4-1.24-.97l-.76-3.03h-5c-.55 0-1-.45-1-1v-6c0-.55.45-1 1-1h14c.55 0 1 .45 1 1z" fill="#e6e9ed"/></g><g><path d="m19 57c0 1.1-.9 2-2 2h-10c-.55 0-1.05-.22-1.41-.59-.37-.36-.59-.86-.59-1.41 0-1.1.45-2.1 1.17-2.83.73-.72 1.73-1.17 2.83-1.17l.11-.21c.54-1.1 1.66-1.79 2.89-1.79s2.35.69 2.89 1.79l.11.21c1.1 0 2.1.45 2.83 1.17.72.73 1.17 1.73 1.17 2.83z" fill="#e6e9ed"/></g><g><path d="m12 32c.64.64 1 1.51 1 2.41 0 1.01-.45 1.96-1.22 2.61l-4.78 3.98-4.78-3.98c-.77-.65-1.22-1.6-1.22-2.61 0-.9.36-1.77 1-2.41s1.51-1 2.41-1h.18c.9 0 1.77.36 2.41 1 .64-.64 1.51-1 2.41-1h.18c.9 0 1.77.36 2.41 1z" fill="#ff826e"/></g><g><path d="m34.11 7.55c.54.28.89.84.89 1.45s-.35 1.17-.89 1.45l-4.77 2.38c-.22.11-.47.17-.72.17-.9 0-1.62-.72-1.62-1.62v-4.76c0-.9.72-1.62 1.62-1.62.25 0 .5.06.72.17z" fill="#ff826e"/></g></g><g><path d="m43.949 34h4.051v-2h-4.051c-.159-2.077-.809-4.013-1.832-5.704l5.59-5.589-1.414-1.414-5.35 5.349c-2.185-2.595-5.363-4.317-8.943-4.592v-2.11c4.493-.5 8-4.317 8-8.941 0-4.962-4.037-9-9-9s-9 4.038-9 9c0 4.624 3.507 8.442 8 8.941v2.11c-3.58.275-6.759 1.997-8.943 4.592l-5.35-5.35-1.414 1.414 5.59 5.59c-1.023 1.691-1.673 3.627-1.832 5.703h-2.051v2h2.051c.275 3.58 1.997 6.759 4.592 8.943l-5.349 5.35 1.414 1.414 5.589-5.59c1.691 1.023 3.627 1.673 5.704 1.832v4.052h2v-4.051c2.076-.159 4.013-.809 5.704-1.832l5.589 5.59 1.414-1.414-5.349-5.35c2.593-2.184 4.316-5.363 4.59-8.943zm-11.259 9.856 1.416-2.831c.484-.969.876-1.983 1.178-3.025h5.502c-1.578 3.076-4.559 5.307-8.096 5.856zm-12.69-10.856c0-1.041.155-2.045.426-3h5.831c-.168.991-.257 1.995-.257 3s.089 2.009.257 3h-5.831c-.271-.955-.426-1.959-.426-3zm8 0c0-1.007.122-2.01.312-3h5.376c.19.99.312 1.993.312 3s-.122 2.01-.312 3h-5.376c-.19-.99-.312-1.993-.312-3zm3-9.763 1.316 2.633c.343.685.617 1.402.857 2.13h-4.347c.24-.728.514-1.445.857-2.13zm11 9.763c0 1.041-.155 2.045-.426 3h-5.831c.168-.991.257-1.995.257-3s-.089-2.009-.257-3h5.831c.271.955.426 1.959.426 3zm-8.827 5c-.24.728-.514 1.445-.857 2.13l-1.316 2.633-1.316-2.633c-.343-.685-.617-1.402-.857-2.13zm7.613-10h-5.502c-.302-1.041-.694-2.056-1.178-3.025l-1.416-2.831c3.537.549 6.518 2.78 8.096 5.856zm-16.786-19c0-3.86 3.141-7 7-7s7 3.14 7 7-3.141 7-7 7-7-3.14-7-7zm5.31 13.144-1.416 2.831c-.484.969-.876 1.983-1.178 3.025h-5.502c1.578-3.076 4.559-5.307 8.096-5.856zm-8.096 15.856h5.502c.302 1.041.694 2.056 1.178 3.025l1.416 2.831c-3.537-.549-6.518-2.78-8.096-5.856z"/><path d="m36 9c0-.998-.555-1.896-1.447-2.342l-4.764-2.382c-.361-.18-.767-.276-1.171-.276-1.443 0-2.618 1.174-2.618 2.618v4.764c0 1.444 1.175 2.618 2.618 2.618.404 0 .81-.096 1.171-.276l4.764-2.382c.892-.446 1.447-1.344 1.447-2.342zm-2.342.553-4.764 2.382c-.386.196-.894-.117-.894-.553v-4.764c0-.341.277-.618.618-.618.096 0 .191.022.276.065l4.764 2.382c.211.106.342.317.342.553s-.131.447-.342.553z"/><path d="m30 60h2v2h-2z"/><path d="m31 56c-2.213 0-4.268 1.1-5.496 2.941l-.336.504 1.664 1.109.336-.504c.856-1.283 2.289-2.05 3.832-2.05s2.976.767 3.832 2.051l.336.504 1.664-1.109-.336-.504c-1.228-1.842-3.283-2.942-5.496-2.942z"/><path d="m31 52c-2.847 0-5.522 1.108-7.534 3.121l-.172.171 1.412 1.416.173-.172c1.635-1.636 3.809-2.536 6.121-2.536s4.486.9 6.122 2.537l.172.171 1.412-1.416-.171-.17c-2.013-2.014-4.688-3.122-7.535-3.122z"/><path d="m58.879 37c-.822 0-1.626.333-2.207.914l-.022.022c-.379-.142-.65-.508-.65-.936v-.382l.081-.041c1.184-.591 1.919-1.781 1.919-3.105 0-1.914-1.558-3.472-3.472-3.472-2.497 0-4.528 2.031-4.528 4.528v1.351c0 4.478 3.644 8.121 8.121 8.121 2.139 0 3.879-1.74 3.879-3.879 0-1.721-1.4-3.121-3.121-3.121zm-.758 5c-3.375 0-6.121-2.746-6.121-6.121v-1.351c0-1.394 1.134-2.528 2.528-2.528.812 0 1.472.661 1.472 1.472 0 .561-.312 1.065-.813 1.316l-1.187.594v1.618c0 1.654 1.346 3 3 3h.414l.672-.672c.209-.208.498-.328.793-.328.618 0 1.121.503 1.121 1.121 0 1.036-.843 1.879-1.879 1.879z"/><path d="m55.953 54.461.719-.719c.856-.856 1.328-1.994 1.328-3.206 0-2.501-2.034-4.536-4.535-4.536h-2.93c-2.501 0-4.535 2.035-4.535 4.536 0 1.212.472 2.351 1.328 3.208l.719.718c.11.946.537 1.792 1.191 2.419-.934.651-1.643 1.618-1.935 2.787l-.584 2.332h10.563l-.584-2.332c-.292-1.169-1-2.136-1.935-2.787.653-.627 1.08-1.474 1.19-2.42zm-5.418-6.461h2.93c1.397 0 2.535 1.138 2.535 2.536 0 .462-.131.9-.361 1.287-.316-.51-.817-.898-1.432-1.052l-2.753-.688c-.222-.055-.447-.083-.673-.083-1.121 0-2.085.671-2.524 1.629-.162-.338-.257-.707-.257-1.093 0-1.398 1.138-2.536 2.535-2.536zm4.18 12h-5.43c.367-1.186 1.462-2 2.715-2s2.348.814 2.715 2zm-4.715-6v-1.219c0-.431.351-.781.781-.781.063 0 .127.008.189.023l2.752.688c.164.041.278.187.278.356v.933c0 1.103-.897 2-2 2s-2-.897-2-2z"/><path d="m15.622 52.039c-.765-1.266-2.123-2.039-3.622-2.039s-2.857.773-3.622 2.039c-2.465.307-4.378 2.415-4.378 4.961 0 1.654 1.346 3 3 3h10c1.654 0 3-1.346 3-3 0-2.546-1.913-4.654-4.378-4.961zm1.378 5.961h-10c-.552 0-1-.449-1-1 0-1.654 1.346-3 3-3h.618l.382-.764c.381-.762 1.147-1.236 2-1.236s1.619.474 2 1.236l.382.764h.618c1.654 0 3 1.346 3 3 0 .551-.448 1-1 1z"/><path d="m12.418 37.787c1.005-.838 1.582-2.07 1.582-3.379 0-1.174-.457-2.278-1.293-3.115-.834-.834-1.942-1.293-3.121-1.293h-.172c-.87 0-1.702.25-2.414.717-.712-.467-1.544-.717-2.414-.717h-.172c-1.179 0-2.287.459-3.127 1.298-.83.831-1.287 1.936-1.287 3.11 0 1.31.577 2.541 1.582 3.379l5.418 4.515zm-10.418-3.379c0-.64.25-1.243.707-1.7.456-.457 1.063-.708 1.707-.708h.172c.645 0 1.251.251 1.707.708l.707.706.707-.706c.456-.457 1.063-.708 1.707-.708h.172c.645 0 1.251.251 1.711.711.453.454.703 1.057.703 1.697 0 .714-.314 1.386-.863 1.843l-4.137 3.447-4.137-3.447c-.549-.457-.863-1.129-.863-1.843z"/><path d="m4 10h2v2h-2z"/><path d="m8 10h2v2h-2z"/><path d="m12 10h2v2h-2z"/><path d="m2 16h4.22l.568 2.272c.254 1.018 1.164 1.728 2.212 1.728s1.958-.71 2.212-1.728l.568-2.272h4.22c1.103 0 2-.897 2-2v-6c0-1.103-.897-2-2-2h-14c-1.103 0-2 .897-2 2v6c0 1.103.897 2 2 2zm0-8h14v6h-5.78l-.947 3.787c-.063.251-.482.251-.545 0l-.948-3.787h-5.78z"/><path d="m55.962 7.625c-.176-.924-.988-1.625-1.962-1.625h-8c-1.103 0-2 .897-2 2v8c0 1.103.897 2 2 2h8c.974 0 1.786-.701 1.962-1.625l6.038 2.012v-12.774zm-9.962 8.375v-8h8l.001 8zm14-.387-4-1.334v-4.558l4-1.334z"/></g></g></svg>+ \ 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 @@ -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 Binary files differ. diff --git a/damus/Assets.xcassets/gradient-backgrounds/Contents.json 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 @@ -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 Binary files 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 @@ -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 Binary files differ. diff --git 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 @@ -101,7 +101,7 @@ struct NonImageAvatar<Content: View>: 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 @@ -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 @@ -2,6 +2,10 @@ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> + <key>CFBundleDocumentTypes</key> + <array> + <dict/> + </array> <key>CFBundleURLTypes</key> <array> <dict> diff --git 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 @@ -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 @@ -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 @@ -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 @@ -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<DisplayTabBarNotify> { + .init() + } +} + +extension Notifications { + static func display_tabbar(_ payload: Bool) -> Notifications<DisplayTabBarNotify> { + .init(.init(payload: payload)) + } +} diff --git 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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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"