Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Paywalls Components Packages and Selected State #4249

Open
wants to merge 142 commits into
base: main
Choose a base branch
from

Conversation

jamesrb1
Copy link
Contributor

@jamesrb1 jamesrb1 commented Sep 6, 2024

This PR introduces package components and the ability for components to have a selected state. They are together because users select a package.

Selection

Models

Components now have an optional property of their own type, eg TextComponent now has a property

public let selectedComponent: TextComponent?

which is a duplicate of the component, but with different properties to visually represent the selected state.

The model classes are class objects so that they can hold recursive self-references, and made final for concurrency.

ViewModels

View model properties are now functions, where the accessor (ie the view) requests the property based on selection state, eg:

func color(for selectionState: SelectionState) -> Color {
    currentComponent(for: selectionState).color.toDyanmicColor()
}

where

private func currentComponent(for selectionState: SelectionState) -> PaywallComponent.TextComponent {
    switch selectionState {
    case .selected:
        return component.selectedComponent ?? component
    case .unselected:
        return component
    }
}

Views

Views now have an environment variable, @Environment(\.selectionState) var selectionState, which they use to read their selection state. An example of how it's used:

Text(viewModel.text(for: selectionState))
    .foregroundStyle(viewModel.color(for: selectionState))

You may wonder if we would be better off adding selectionState directly to the view model, so that the view did not need to repeatedly pass in this state. The reason we don't do this is because SwiftUI is designed to handle transient view state like selection directly in the view itself. A good common example of this is the state that holds if a sheet should be presented. By keeping this type of state in the view, the viewmodel is simplified by being stateless (to this sort of state), and able to concern itself solely with the preparation of business logic for display. The view decides which business logic to present.

Packages

There are two new components here: PackageGroupComponent and PackageComponent.

Package:

These are the individual buttons representing a particular package that the user can select to purchase.

PackageComponent

Comes with a packageID, and a list of other components which are used to construct its visual appearance.

PackageComponentViewModel

When the PackageComponentViewModel is created, it validates that the package is available from the Offering. If not, it throws, and the paywall cannot be displayed (it displays a fallback paywall instead). This is the same system as is done for localization.

PackageComponentView

The view is typically a Button, where the action sets the selected package ID, and the label is its list of components:

var body: some View {
    Button {
        selectionManager.selectedID = viewModel.packageID
    } label: {
        ComponentsView(componentViewModels: self.viewModel.viewModels)
            .environment(\.selectionState, selectionState)
    }
}

It can also be created as a non-button (by showing the components directly), for use when the package is to be selected in a manner other than tapping on it (such as in a carousel).

It determines the selectionState for the components displayed in its label by comparing its package's ID to the currently selected one:

var selectionState: SelectionState {
    return selectionManager.selectedID == viewModel.packageID ? .selected : .unselected
}

PackageGroup:

The purpose of the Package Group is associate a group of packages together for selection purposes (where selecting one will unselect the others), to hold the value of the currently selected package, and to associate a Purchase Button with a package for purchase. It has no visual appearance.

PackageGroupComponent

Comes with a defaultSelectedPackageID and list of components to display

PackageGroupComponentViewModel

Nothing of specific note here.

PackageGroupComponentView

This holds a reference to a PackageSelectionManager, which which holds the value of the currently selected packageID, and which it passes down through the environment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PurchaseButton is a stand-in for now (it doesn't work, obviously). But it does know what package it is meant to purchase.

@jamesrb1 jamesrb1 requested review from a team September 13, 2024 18:30
@jamesrb1 jamesrb1 changed the title [WIP] Paywalls Components Packages and Selected State Paywalls Components Packages and Selected State Sep 13, 2024
@jamesrb1 jamesrb1 marked this pull request as ready for review September 13, 2024 18:31
More work here needed in this area at some point.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants