mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #730 from omnivore-app/feature/update-welcome-screen
Update welcome screen
This commit is contained in:
commit
4771d11c51
19 changed files with 207 additions and 317 deletions
|
|
@ -135,7 +135,7 @@ struct ProfileView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private extension BasicWebAppView {
|
||||
extension BasicWebAppView {
|
||||
static func privacyPolicyWebView(baseURL: URL) -> BasicWebAppView {
|
||||
omnivoreWebView(path: "/app/privacy", baseURL: baseURL)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,69 +84,10 @@ final class RegistrationViewModel: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
struct RegistrationView: View {
|
||||
@EnvironmentObject var authenticator: Authenticator
|
||||
@EnvironmentObject var dataService: DataService
|
||||
@Environment(\.horizontalSizeClass) var horizontalSizeClass
|
||||
@StateObject private var viewModel = RegistrationViewModel()
|
||||
|
||||
var authenticationView: some View {
|
||||
VStack(spacing: 0) {
|
||||
VStack(spacing: 28) {
|
||||
if horizontalSizeClass == .regular {
|
||||
Spacer()
|
||||
}
|
||||
|
||||
VStack(alignment: .center, spacing: 16) {
|
||||
Text(LocalText.registrationViewHeadline)
|
||||
.font(.appTitle)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.bottom, horizontalSizeClass == .compact ? 0 : 50)
|
||||
.padding(.top, horizontalSizeClass == .compact ? 30 : 0)
|
||||
|
||||
AppleSignInButton {
|
||||
viewModel.handleAppleSignInCompletion(result: $0, authenticator: authenticator)
|
||||
}
|
||||
|
||||
if AppKeys.sharedInstance?.iosClientGoogleId != nil {
|
||||
GoogleAuthButton {
|
||||
viewModel.handleGoogleAuth(authenticator: authenticator)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let loginError = viewModel.loginError {
|
||||
LoginErrorMessageView(loginError: loginError)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: 316)
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if let registrationState = viewModel.registrationState {
|
||||
if case let RegistrationViewModel.RegistrationState.createProfile(userProfile) = registrationState {
|
||||
CreateProfileView(userProfile: userProfile)
|
||||
} else if case let RegistrationViewModel.RegistrationState.newAppleSignUp(userProfile) = registrationState {
|
||||
NewAppleSignupView(
|
||||
userProfile: userProfile,
|
||||
showProfileEditView: { viewModel.registrationState = .createProfile(userProfile: userProfile) }
|
||||
)
|
||||
} else {
|
||||
authenticationView
|
||||
}
|
||||
} else {
|
||||
authenticationView
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func presentingViewController() -> PlatformViewController? {
|
||||
#if os(iOS)
|
||||
return UIApplication.shared.windows
|
||||
let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene
|
||||
return scene?.windows
|
||||
.filter(\.isKeyWindow)
|
||||
.first?
|
||||
.rootViewController
|
||||
|
|
|
|||
|
|
@ -7,11 +7,19 @@ import Views
|
|||
|
||||
struct WelcomeView: View {
|
||||
@EnvironmentObject var dataService: DataService
|
||||
@EnvironmentObject var authenticator: Authenticator
|
||||
@Environment(\.horizontalSizeClass) var horizontalSizeClass
|
||||
|
||||
@StateObject private var viewModel = RegistrationViewModel()
|
||||
|
||||
@State private var showRegistrationView = false
|
||||
@State private var isKeyboardOnScreen = false
|
||||
@State private var showDebugModal = false
|
||||
@State private var showTermsLinks = false
|
||||
@State private var showTermsModal = false
|
||||
@State private var showPrivacyModal = false
|
||||
@State private var showAboutPage = false
|
||||
@State private var selectedEnvironment = AppEnvironment.initialAppEnvironment
|
||||
@State private var containerSize: CGSize = .zero
|
||||
|
||||
func handleHiddenGestureAction() {
|
||||
if !Bundle.main.isAppStoreBuild {
|
||||
|
|
@ -19,68 +27,183 @@ struct WelcomeView: View {
|
|||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func userInteractiveView(width: CGFloat) -> some View {
|
||||
var headlineText: some View {
|
||||
Group {
|
||||
if showRegistrationView {
|
||||
RegistrationView()
|
||||
if horizontalSizeClass == .compact {
|
||||
Text("Everything you read. Safe, organized, and easy to share.")
|
||||
} else {
|
||||
GetStartedView(showRegistrationView: $showRegistrationView)
|
||||
Text("Everything you read. Safe,\norganized, and easy to share.")
|
||||
}
|
||||
}
|
||||
.frame(width: width)
|
||||
.zIndex(2)
|
||||
.font(.appLargeTitle)
|
||||
}
|
||||
|
||||
@ViewBuilder func primaryContent() -> some View {
|
||||
if horizontalSizeClass == .compact {
|
||||
GeometryReader { geometry in
|
||||
ZStack(alignment: .leading) {
|
||||
Color.systemBackground
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
var headlineView: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
headlineText
|
||||
|
||||
if geometry.size.width < geometry.size.height, !isKeyboardOnScreen {
|
||||
VStack {
|
||||
Color.appDeepBackground.frame(height: 100)
|
||||
Spacer()
|
||||
}
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
}
|
||||
|
||||
VStack {
|
||||
if geometry.size.width < geometry.size.height, !isKeyboardOnScreen {
|
||||
RegistrationHeroImageView(tapGestureHandler: handleHiddenGestureAction)
|
||||
}
|
||||
userInteractiveView(width: geometry.size.width)
|
||||
Spacer()
|
||||
Button(
|
||||
action: { showAboutPage = true },
|
||||
label: {
|
||||
HStack(spacing: 4) {
|
||||
Text("Learn more")
|
||||
Image(systemName: "arrow.right")
|
||||
}
|
||||
.font(.appTitleThree)
|
||||
}
|
||||
)
|
||||
.foregroundColor(.appGrayTextContrast)
|
||||
}
|
||||
}
|
||||
|
||||
var footerView: some View {
|
||||
Group {
|
||||
Text("By signing up, you agree to Omnivore’s\n")
|
||||
+ Text("Terms of Service").underline()
|
||||
+ Text(" and ")
|
||||
+ Text("Privacy Policy").underline()
|
||||
}
|
||||
.font(.appSubheadline)
|
||||
.confirmationDialog("", isPresented: $showTermsLinks, titleVisibility: .hidden) {
|
||||
Button("View Terms of Service") {
|
||||
showTermsModal = true
|
||||
}
|
||||
} else {
|
||||
GeometryReader { geometry in
|
||||
ZStack(alignment: .leading) {
|
||||
SplitColorBackground(width: geometry.size.width)
|
||||
|
||||
VStack {
|
||||
TitleLogoView(handleHiddenGestureAction: handleHiddenGestureAction)
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
Button("View Privacy Policy") {
|
||||
showPrivacyModal = true
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showPrivacyModal) {
|
||||
VStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
Button(
|
||||
action: {
|
||||
showPrivacyModal = false
|
||||
},
|
||||
label: {
|
||||
Image(systemName: "xmark.circle").foregroundColor(.appGrayTextContrast)
|
||||
}
|
||||
)
|
||||
}
|
||||
.padding()
|
||||
BasicWebAppView.privacyPolicyWebView(baseURL: dataService.appEnvironment.webAppBaseURL)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showTermsModal) {
|
||||
VStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
Button(
|
||||
action: {
|
||||
showTermsModal = false
|
||||
},
|
||||
label: {
|
||||
Image(systemName: "xmark.circle").foregroundColor(.appGrayTextContrast)
|
||||
}
|
||||
)
|
||||
}
|
||||
.padding()
|
||||
BasicWebAppView.termsConditionsWebView(baseURL: dataService.appEnvironment.webAppBaseURL)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showAboutPage) {
|
||||
if let url = URL(string: "https://omnivore.app/about") {
|
||||
SafariView(url: url)
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
showTermsLinks = true
|
||||
}
|
||||
}
|
||||
|
||||
HStack(spacing: 0) {
|
||||
userInteractiveView(width: geometry.size.width * 0.5)
|
||||
ReadingIllustrationXXLView(width: geometry.size.width * 0.5)
|
||||
var logoView: some View {
|
||||
Image.omnivoreTitleLogo
|
||||
.gesture(
|
||||
TapGesture(count: 2)
|
||||
.onEnded {
|
||||
if !Bundle.main.isAppStoreBuild {
|
||||
showDebugModal = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
var authProviderButtonStack: some View {
|
||||
let useHorizontalLayout = containerSize.width > 400
|
||||
|
||||
let buttonGroup = Group {
|
||||
AppleSignInButton {
|
||||
viewModel.handleAppleSignInCompletion(result: $0, authenticator: authenticator)
|
||||
}
|
||||
|
||||
if AppKeys.sharedInstance?.iosClientGoogleId != nil {
|
||||
GoogleAuthButton {
|
||||
viewModel.handleGoogleAuth(authenticator: authenticator)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
VStack(alignment: .center, spacing: 16) {
|
||||
if useHorizontalLayout {
|
||||
HStack { buttonGroup }
|
||||
} else {
|
||||
buttonGroup
|
||||
}
|
||||
|
||||
if let loginError = viewModel.loginError {
|
||||
HStack {
|
||||
LoginErrorMessageView(loginError: loginError)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
primaryContent()
|
||||
.sheet(isPresented: $showDebugModal) {
|
||||
DebugMenuView(selectedEnvironment: $selectedEnvironment)
|
||||
ZStack(alignment: .leading) {
|
||||
Color.appBackground
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
.modifier(SizeModifier())
|
||||
.onPreferenceChange(SizePreferenceKey.self) {
|
||||
self.containerSize = $0
|
||||
}
|
||||
if let registrationState = viewModel.registrationState {
|
||||
if case let RegistrationViewModel.RegistrationState.createProfile(userProfile) = registrationState {
|
||||
CreateProfileView(userProfile: userProfile)
|
||||
} else if case let RegistrationViewModel.RegistrationState.newAppleSignUp(userProfile) = registrationState {
|
||||
NewAppleSignupView(
|
||||
userProfile: userProfile,
|
||||
showProfileEditView: { viewModel.registrationState = .createProfile(userProfile: userProfile) }
|
||||
)
|
||||
} else {
|
||||
EmptyView() // will never be called
|
||||
}
|
||||
} else {
|
||||
VStack(alignment: .leading, spacing: containerSize.height < 500 ? 12 : 50) {
|
||||
logoView
|
||||
.padding(.bottom, 20)
|
||||
headlineView
|
||||
if containerSize.width > 400 {
|
||||
authProviderButtonStack
|
||||
} else {
|
||||
HStack {
|
||||
Spacer()
|
||||
authProviderButtonStack
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
footerView
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
.sheet(isPresented: $showDebugModal) {
|
||||
DebugMenuView(selectedEnvironment: $selectedEnvironment)
|
||||
}
|
||||
}
|
||||
.onReceive(Publishers.keyboardHeight) { isKeyboardOnScreen = $0 > 1 }
|
||||
.onAppear { selectedEnvironment = dataService.appEnvironment }
|
||||
}
|
||||
.preferredColorScheme(.light)
|
||||
.task { selectedEnvironment = dataService.appEnvironment }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,8 +18,9 @@ public struct AppleSignInButton: View {
|
|||
},
|
||||
onCompletion: onCompletion
|
||||
)
|
||||
.frame(height: 44)
|
||||
.frame(height: 54)
|
||||
.frame(maxWidth: 300)
|
||||
.cornerRadius(8)
|
||||
.signInWithAppleButtonStyle(colorScheme == .dark ? .white : .black)
|
||||
.signInWithAppleButtonStyle(.white)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,13 +12,14 @@ public struct GoogleAuthButton: View {
|
|||
HStack(spacing: 8) {
|
||||
Image.googleIcon
|
||||
.resizable()
|
||||
.frame(width: 12, height: 12)
|
||||
.frame(width: 16, height: 16)
|
||||
Text(LocalText.googleAuthButton)
|
||||
.font(isMacApp ? .appCaption : .appBody)
|
||||
.font(isMacApp ? .appCaption : .appTitleThree)
|
||||
.foregroundColor(.black)
|
||||
.fontWeight(Font.Weight.medium)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: isMacApp ? 30 : 44)
|
||||
.frame(maxWidth: 300)
|
||||
.frame(height: isMacApp ? 30 : 54)
|
||||
}
|
||||
.buttonStyle(GoogleButtonStyle())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x9F",
|
||||
"green" : "0xEA",
|
||||
"red" : "0xFF"
|
||||
"blue" : "0xA8",
|
||||
"green" : "0xEB",
|
||||
"red" : "0xFB"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
|
@ -23,9 +23,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x9F",
|
||||
"green" : "0xEA",
|
||||
"red" : "0xFF"
|
||||
"blue" : "0xA8",
|
||||
"green" : "0xEB",
|
||||
"red" : "0xFB"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
|
|
|||
|
|
@ -3,10 +3,7 @@ import SwiftUI
|
|||
public extension Image {
|
||||
static var smallOmnivoreLogo: Image { Image("_smallOmnivoreLogo", bundle: .module) }
|
||||
static var omnivoreTitleLogo: Image { Image("_omnivoreTitleLogo", bundle: .module) }
|
||||
static var readingIllustration: Image { Image("_readingIllustration", bundle: .module) }
|
||||
static var readingIllustrationXXL: Image { Image("_readingIllustrationXXL", bundle: .module) }
|
||||
static var googleIcon: Image { Image("_googleIcon", bundle: .module) }
|
||||
static var feedItemPlaceholder: Image { Image("_feedItemPlaceholder", bundle: .module) }
|
||||
static var sunHorizon: Image { Image("_sun-horizon", bundle: .module) }
|
||||
static var mountains: Image { Image("_mountains", bundle: .module) }
|
||||
static var moon: Image { Image("_moon", bundle: .module) }
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "imagePlaceholder.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
|
@ -1,23 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Mask Group.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Mask Group@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Mask Group@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 75 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 274 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 614 KiB |
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "XXLIllustration.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 MiB |
|
|
@ -1,25 +0,0 @@
|
|||
import SwiftUI
|
||||
|
||||
public struct RegistrationHeroImageView: View {
|
||||
let tapGestureHandler: () -> Void
|
||||
|
||||
public init(tapGestureHandler: @escaping () -> Void) {
|
||||
self.tapGestureHandler = tapGestureHandler
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
ZStack(alignment: .topLeading) {
|
||||
Image.readingIllustration
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
Image.omnivoreTitleLogo
|
||||
.padding()
|
||||
.gesture(
|
||||
TapGesture(count: 2)
|
||||
.onEnded {
|
||||
tapGestureHandler()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
import Models
|
||||
import SwiftUI
|
||||
|
||||
struct ToggleAuthFlowButton: View {
|
||||
let authFlow: AuthFlow
|
||||
let action: () -> Void
|
||||
|
||||
var buttonTitle: String {
|
||||
switch authFlow {
|
||||
case .signIn:
|
||||
return "Don’t have an account? "
|
||||
case .signUp:
|
||||
return "Already have an account? "
|
||||
}
|
||||
}
|
||||
|
||||
var buttonTitleSuffix: String {
|
||||
switch authFlow {
|
||||
case .signIn:
|
||||
return "Sign Up"
|
||||
case .signUp:
|
||||
return "Log In"
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button(
|
||||
action: action,
|
||||
label: {
|
||||
Text(buttonTitle)
|
||||
.font(.appFootnote)
|
||||
.foregroundColor(.appGrayText)
|
||||
+ Text(buttonTitleSuffix)
|
||||
.underline()
|
||||
.font(.appFootnote)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
}
|
||||
23
apple/OmnivoreKit/Sources/Views/SizeModifier.swift
Normal file
23
apple/OmnivoreKit/Sources/Views/SizeModifier.swift
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import SwiftUI
|
||||
|
||||
public struct SizePreferenceKey: PreferenceKey {
|
||||
public static var defaultValue: CGSize = .zero
|
||||
|
||||
public static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
|
||||
value = nextValue()
|
||||
}
|
||||
}
|
||||
|
||||
public struct SizeModifier: ViewModifier {
|
||||
public init() {}
|
||||
|
||||
private var sizeView: some View {
|
||||
GeometryReader { geometry in
|
||||
Color.clear.preference(key: SizePreferenceKey.self, value: geometry.size)
|
||||
}
|
||||
}
|
||||
|
||||
public func body(content: Content) -> some View {
|
||||
content.background(sizeView)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
import SwiftUI
|
||||
|
||||
public struct ReadingIllustrationXXLView: View {
|
||||
let width: CGFloat
|
||||
|
||||
public init(width: CGFloat) {
|
||||
self.width = width
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
Image.readingIllustrationXXL
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: width)
|
||||
.clipped()
|
||||
.edgesIgnoringSafeArea([.vertical, .trailing])
|
||||
}
|
||||
}
|
||||
|
||||
public struct TitleLogoView: View {
|
||||
let handleHiddenGestureAction: () -> Void
|
||||
|
||||
public init(handleHiddenGestureAction: @escaping () -> Void) {
|
||||
self.handleHiddenGestureAction = handleHiddenGestureAction
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
Image.omnivoreTitleLogo
|
||||
.renderingMode(.template)
|
||||
.foregroundColor(.appGrayTextContrast)
|
||||
.frame(height: 40)
|
||||
.gesture(
|
||||
TapGesture(count: 2)
|
||||
.onEnded {
|
||||
handleHiddenGestureAction()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public struct GetStartedView: View {
|
||||
@Environment(\.horizontalSizeClass) var horizontalSizeClass
|
||||
@Binding var showRegistrationView: Bool
|
||||
|
||||
public init(showRegistrationView: Binding<Bool>) {
|
||||
self._showRegistrationView = showRegistrationView
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 32) {
|
||||
Text("A better social\nreading experience\nstarts with Omnivore.")
|
||||
.font(.appTitle)
|
||||
.multilineTextAlignment(.leading)
|
||||
|
||||
BorderedButton(color: .appGrayTextContrast, text: "Get Started") {
|
||||
showRegistrationView = true
|
||||
}
|
||||
.frame(width: 220)
|
||||
}
|
||||
.padding(.leading, horizontalSizeClass == .compact ? 16 : 80)
|
||||
.padding(.top, horizontalSizeClass == .compact ? 16 : 0)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct SplitColorBackground: View {
|
||||
let width: CGFloat
|
||||
|
||||
public init(width: CGFloat) {
|
||||
self.width = width
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
Color.systemBackground.frame(width: width * 0.5)
|
||||
Color.appBackground.frame(width: width * 0.5)
|
||||
}
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue