diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cab402d5..0889c472f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Increase build settings timeout in fastlane. [#1662](https://github.com/planetary-social/nos/pull/1662) - Removed new moderation feature flag. [#1646](https://github.com/planetary-social/nos/issues/1646) - Added the Private Key onboarding screen. Currently behind the “New Onboarding Flow” feature flag. [#1595](https://github.com/planetary-social/nos/issues/1595) +- Added the Public Key onboarding screen. Currently behind the “New Onboarding Flow” feature flag. [#1596](https://github.com/planetary-social/nos/issues/1596) ## [0.2.2] - 2024-10-11Z diff --git a/Nos.xcodeproj/project.pbxproj b/Nos.xcodeproj/project.pbxproj index c85fc1694..984826b26 100644 --- a/Nos.xcodeproj/project.pbxproj +++ b/Nos.xcodeproj/project.pbxproj @@ -16,6 +16,9 @@ 0304D0B22C9B731F001D16C7 /* MockOpenGraphService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0304D0B12C9B731F001D16C7 /* MockOpenGraphService.swift */; }; 0304D0B32C9B731F001D16C7 /* MockOpenGraphService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0304D0B12C9B731F001D16C7 /* MockOpenGraphService.swift */; }; 030AE4292BE3D63C004DEE02 /* FeaturedAuthor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030AE4282BE3D63C004DEE02 /* FeaturedAuthor.swift */; }; + 030E56CA2CC1BC6200A4A51E /* PublicKeyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030E56C92CC1BC6200A4A51E /* PublicKeyView.swift */; }; + 030E56E42CC1BF2900A4A51E /* CopyButtonState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030E56E32CC1BF2900A4A51E /* CopyButtonState.swift */; }; + 030E56F32CC2836D00A4A51E /* CopyKeyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030E56F22CC2836D00A4A51E /* CopyKeyView.swift */; }; 030FECAB2CB5E0B900820014 /* BuildYourNetworkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030FECAA2CB5E0B900820014 /* BuildYourNetworkView.swift */; }; 0314CF742C9C7DD00001A53B /* youTube_fortnight_short.html in Resources */ = {isa = PBXBuildFile; fileRef = 0314CF732C9C7DD00001A53B /* youTube_fortnight_short.html */; }; 0314D5AC2C7D31060002E7F4 /* MediaService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0314D5AB2C7D31060002E7F4 /* MediaService.swift */; }; @@ -589,6 +592,9 @@ 0304D0A62C9B4BF2001D16C7 /* OpenGraphMetatdata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGraphMetatdata.swift; sourceTree = ""; }; 0304D0B12C9B731F001D16C7 /* MockOpenGraphService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockOpenGraphService.swift; sourceTree = ""; }; 030AE4282BE3D63C004DEE02 /* FeaturedAuthor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeaturedAuthor.swift; sourceTree = ""; }; + 030E56C92CC1BC6200A4A51E /* PublicKeyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicKeyView.swift; sourceTree = ""; }; + 030E56E32CC1BF2900A4A51E /* CopyButtonState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyButtonState.swift; sourceTree = ""; }; + 030E56F22CC2836D00A4A51E /* CopyKeyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyKeyView.swift; sourceTree = ""; }; 030FECAA2CB5E0B900820014 /* BuildYourNetworkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildYourNetworkView.swift; sourceTree = ""; }; 0314CF732C9C7DD00001A53B /* youTube_fortnight_short.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = youTube_fortnight_short.html; sourceTree = ""; }; 0314D5AB2C7D31060002E7F4 /* MediaService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaService.swift; sourceTree = ""; }; @@ -1054,6 +1060,15 @@ path = CoreData; sourceTree = ""; }; + 030E56E52CC2835A00A4A51E /* Components */ = { + isa = PBXGroup; + children = ( + 030E56F22CC2836D00A4A51E /* CopyKeyView.swift */, + 03C5DBC42CC19044009A9E0E /* LargeNumberView.swift */, + ); + path = Components; + sourceTree = ""; + }; 0315B5ED2C7E44FD0020E707 /* Media */ = { isa = PBXGroup; children = ( @@ -1463,13 +1478,14 @@ children = ( 030FECAA2CB5E0B900820014 /* BuildYourNetworkView.swift */, 039F09582CC051EE00FEEC81 /* CreateAccountView.swift */, - 03C5DBC42CC19044009A9E0E /* LargeNumberView.swift */, 3F30020629C237AB003D4F8B /* OnboardingAgeVerificationView.swift */, 3F30020C29C382EB003D4F8B /* OnboardingLoginView.swift */, 3F30020829C23895003D4F8B /* OnboardingNotOldEnoughView.swift */, 3F30020429C1FDD9003D4F8B /* OnboardingStartView.swift */, 3FB5E650299D28A200386527 /* OnboardingView.swift */, 038EF09C2CC16D640031F7F2 /* PrivateKeyView.swift */, + 030E56C92CC1BC6200A4A51E /* PublicKeyView.swift */, + 030E56E52CC2835A00A4A51E /* Components */, ); path = Onboarding; sourceTree = ""; @@ -1542,6 +1558,7 @@ C92F01502AC4D67B00972489 /* Form */, 03C8B4902C6D061900A07CCD /* Media */, 03618B1E2C825F0900BCBC55 /* Wizard */, + 030E56E32CC1BF2900A4A51E /* CopyButtonState.swift */, ); path = Components; sourceTree = ""; @@ -2322,6 +2339,7 @@ 502B6C3D2C9462A400446316 /* PushNotificationRegistrar.swift in Sources */, 5B6EB48E29EDBE0E006E750C /* NoteParser.swift in Sources */, C9F84C23298DC7B900C6714D /* SettingsView.swift in Sources */, + 030E56E42CC1BF2900A4A51E /* CopyButtonState.swift in Sources */, 03E711812C936DD1000B6F96 /* OpenGraphParser.swift in Sources */, 03C8B4962C6D065900A07CCD /* ImageViewer.swift in Sources */, 5B79F6092B98AC33002DA9BE /* ClaimYourUniqueIdentitySheet.swift in Sources */, @@ -2352,6 +2370,7 @@ C9DFA972299BF9E8006929C1 /* CompactNoteView.swift in Sources */, C9AC31AD2A55E0BD00A94E5A /* NotificationViewModel.swift in Sources */, 0326347A2C10C57A00E489B5 /* FileStorageAPIClient.swift in Sources */, + 030E56CA2CC1BC6200A4A51E /* PublicKeyView.swift in Sources */, C9EE3E632A053910008A7491 /* ExpirationTimeOption.swift in Sources */, 03C7E7A22CB9CD150054624C /* PointDownEmojiTipViewStyle.swift in Sources */, C9A0DAE029C697A100466635 /* AboutView.swift in Sources */, @@ -2490,6 +2509,7 @@ 5B834F692A83FC7F000C1432 /* ProfileSocialStatsView.swift in Sources */, 045EDD052CAC025700B67964 /* ScrollViewProxy+Animate.swift in Sources */, CD09A74629A50F750063464F /* SideMenuContent.swift in Sources */, + 030E56F32CC2836D00A4A51E /* CopyKeyView.swift in Sources */, C9DFA971299BF8CD006929C1 /* NoteView.swift in Sources */, 037071272C90C5FA00BEAEC4 /* OpenGraphService.swift in Sources */, C974652E2A3B86600031226F /* NoteCardHeader.swift in Sources */, diff --git a/Nos/Assets/Localization/Localizable.xcstrings b/Nos/Assets/Localization/Localizable.xcstrings index 32b9059e6..ae83eee0a 100644 --- a/Nos/Assets/Localization/Localizable.xcstrings +++ b/Nos/Assets/Localization/Localizable.xcstrings @@ -3769,6 +3769,18 @@ } } }, + "copyPublicKey" : { + "comment" : "title for the button that copies the public key", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copy public key" + } + } + } + }, "copyQRLink" : { "extractionState" : "manual", "localizations" : { @@ -14539,6 +14551,18 @@ } } }, + "publicKeyDescription" : { + "comment" : "a description for the public key screen explaining what a public key is", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "This is your public identifier that does not change. Public keys can be shared." + } + } + } + }, "publicKeyHeadline" : { "comment" : "headline for the public key screen in onboarding", "extractionState" : "manual", @@ -14551,6 +14575,18 @@ } } }, + "publicKeyNpubParenthetical" : { + "comment" : "the word \"npub\" in parentheses in English. \"npub\" should not be translated, but the parentheses may", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "(npub)" + } + } + } + }, "quote" : { "extractionState" : "manual", "localizations" : { diff --git a/Nos/Views/Components/CopyButtonState.swift b/Nos/Views/Components/CopyButtonState.swift new file mode 100644 index 000000000..c9219eeeb --- /dev/null +++ b/Nos/Views/Components/CopyButtonState.swift @@ -0,0 +1,5 @@ +/// The state of a copy button. +enum CopyButtonState { + case copy + case copied +} diff --git a/Nos/Views/Onboarding/Components/CopyKeyView.swift b/Nos/Views/Onboarding/Components/CopyKeyView.swift new file mode 100644 index 000000000..457ec1296 --- /dev/null +++ b/Nos/Views/Onboarding/Components/CopyKeyView.swift @@ -0,0 +1,63 @@ +import SwiftUI + +/// A bordered view that shows a key and a button to copy it. When the user taps the copy button, +/// its title changes to "Copied!". +struct CopyKeyView: View { + let buttonTitle: LocalizedStringKey + + @Binding var keyString: String + @Binding var copyButtonState: CopyButtonState + + init(_ buttonTitle: LocalizedStringKey, keyString: Binding, copyButtonState: Binding) { + self.buttonTitle = buttonTitle + _keyString = keyString + _copyButtonState = copyButtonState + } + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + Text(keyString) + HStack { + if copyButtonState == .copy { + Image.copyIcon + .frame(width: 20, height: 20) + } else { + Image(systemName: "checkmark") + .frame(width: 20, height: 20) + } + Button { + UIPasteboard.general.string = keyString + copyButtonState = .copied + Task { @MainActor in + try await Task.sleep(for: .seconds(10)) + copyButtonState = .copy + } + } label: { + Text(copyButtonState == .copy ? buttonTitle : "copied") + } + Spacer() + } + .foregroundStyle(Color.actionTertiary) + } + .padding() + .withStyledBorder() + } +} + +#Preview { + @State var privateKey = KeyFixture.nsec + @State var privateCopyButtonState = CopyButtonState.copy + + @State var publicKey = KeyFixture.npub + @State var publicCopyButtonState = CopyButtonState.copied + + return VStack(spacing: 40) { + CopyKeyView("copyPrivateKey", keyString: $privateKey, copyButtonState: $privateCopyButtonState) + CopyKeyView("copyPublicKey", keyString: $publicKey, copyButtonState: $publicCopyButtonState) + } +} + +enum KeyType { + case `public` + case `private` +} diff --git a/Nos/Views/Onboarding/LargeNumberView.swift b/Nos/Views/Onboarding/Components/LargeNumberView.swift similarity index 100% rename from Nos/Views/Onboarding/LargeNumberView.swift rename to Nos/Views/Onboarding/Components/LargeNumberView.swift diff --git a/Nos/Views/Onboarding/OnboardingView.swift b/Nos/Views/Onboarding/OnboardingView.swift index 836bd29ee..7cc9ea1da 100644 --- a/Nos/Views/Onboarding/OnboardingView.swift +++ b/Nos/Views/Onboarding/OnboardingView.swift @@ -21,6 +21,7 @@ enum OnboardingStep { case notOldEnough case createAccount case privateKey + case publicKey case buildYourNetwork case login } @@ -53,6 +54,9 @@ struct OnboardingView: View { case .privateKey: PrivateKeyView() .environment(state) + case .publicKey: + PublicKeyView() + .environment(state) case .login: OnboardingLoginView(completion: completion) case .buildYourNetwork: diff --git a/Nos/Views/Onboarding/PrivateKeyView.swift b/Nos/Views/Onboarding/PrivateKeyView.swift index 4d1c64460..8392d5791 100644 --- a/Nos/Views/Onboarding/PrivateKeyView.swift +++ b/Nos/Views/Onboarding/PrivateKeyView.swift @@ -39,10 +39,10 @@ struct PrivateKeyView: View { .foregroundStyle(Color.secondaryTxt) } PrivateKeyDescription() - BorderedPrivateKey(privateKeyString: $privateKeyString, copyButtonState: $copyButtonState) + CopyKeyView("copyPrivateKey", keyString: $privateKeyString, copyButtonState: $copyButtonState) Spacer() BigActionButton("next") { - state.step = .buildYourNetwork + state.step = .publicKey } } .padding(40) @@ -50,11 +50,6 @@ struct PrivateKeyView: View { } } -fileprivate enum CopyButtonState { - case copy - case copied -} - fileprivate struct PrivateKeyDescription: View { var body: some View { let attributedString = AttributedString(localized: "privateKeyDescription") @@ -73,40 +68,6 @@ fileprivate struct PrivateKeyDescription: View { } } -fileprivate struct BorderedPrivateKey: View { - @Binding var privateKeyString: String - @Binding var copyButtonState: CopyButtonState - - var body: some View { - VStack(alignment: .leading, spacing: 16) { - Text(privateKeyString) - HStack { - if copyButtonState == .copy { - Image.copyIcon - .frame(width: 20, height: 20) - } else { - Image(systemName: "checkmark") - .frame(width: 20, height: 20) - } - Button { - UIPasteboard.general.string = privateKeyString - copyButtonState = .copied - Task { @MainActor in - try await Task.sleep(for: .seconds(10)) - copyButtonState = .copy - } - } label: { - Text(copyButtonState == .copy ? "copyPrivateKey" : "copied") - } - Spacer() - } - .foregroundStyle(Color.actionTertiary) - } - .padding() - .withStyledBorder() - } -} - #Preview { PrivateKeyView() .environment(OnboardingState()) diff --git a/Nos/Views/Onboarding/PublicKeyView.swift b/Nos/Views/Onboarding/PublicKeyView.swift new file mode 100644 index 000000000..91f58f2c3 --- /dev/null +++ b/Nos/Views/Onboarding/PublicKeyView.swift @@ -0,0 +1,59 @@ +import Dependencies +import SwiftUI + +/// The Public Key view in the onboarding. +struct PublicKeyView: View { + @Environment(OnboardingState.self) private var state + @Environment(CurrentUser.self) var currentUser + + @State private var publicKeyString = "" + @State private var copyButtonState: CopyButtonState = .copy + + var body: some View { + ZStack { + Color.appBg + .ignoresSafeArea() + ViewThatFits(in: .vertical) { + publicKeyStack + + ScrollView { + publicKeyStack + } + } + .onAppear { + publicKeyString = currentUser.keyPair?.npub ?? "" + } + } + .navigationBarHidden(true) + } + + var publicKeyStack: some View { + VStack(alignment: .leading, spacing: 20) { + LargeNumberView(2) + HStack(alignment: .firstTextBaseline) { + Text("publicKeyHeadline") + .font(.clarityBold(.title)) + .foregroundStyle(Color.primaryTxt) + Text("publicKeyNpubParenthetical") + .font(.clarityRegular(.title2)) + .foregroundStyle(Color.secondaryTxt) + } + Text("publicKeyDescription") + .font(.body) + .foregroundStyle(Color.secondaryTxt) + CopyKeyView("copyPublicKey", keyString: $publicKeyString, copyButtonState: $copyButtonState) + Spacer() + BigActionButton("next") { + state.step = .buildYourNetwork + } + } + .padding(40) + .readabilityPadding() + } +} + +#Preview { + PublicKeyView() + .environment(OnboardingState()) + .inject(previewData: PreviewData()) +} diff --git a/Nos/Views/Settings/SettingsView.swift b/Nos/Views/Settings/SettingsView.swift index 49f1754b7..5e33982ed 100644 --- a/Nos/Views/Settings/SettingsView.swift +++ b/Nos/Views/Settings/SettingsView.swift @@ -36,11 +36,6 @@ struct SettingsView: View { case deleteAccount } - fileprivate enum CopyButtonState { - case copy - case copied - } - var body: some View { Form { Section {