Login and subscriptions improvements

multistore
Laurent 2 years ago
parent d5dad96aa7
commit f5ae0ef699
  1. 12
      PadelClub.xcodeproj/project.pbxproj
  2. 104
      PadelClub.xcodeproj/xcshareddata/xcschemes/PadelClub.xcscheme
  3. 56
      PadelClub/Assets.xcassets/MasterColor.colorset/Contents.json
  4. 4
      PadelClub/Data/DataStore.swift
  5. 4
      PadelClub/Info.plist
  6. 2
      PadelClub/PadelClubApp.swift
  7. 118
      PadelClub/SyncedProducts.storekit
  8. 5
      PadelClub/Views/Components/StepperView.swift
  9. 2
      PadelClub/Views/Shared/SelectablePlayerListView.swift
  10. 4
      PadelClub/Views/Subscription/Guard.swift
  11. 14
      PadelClub/Views/Subscription/StoreItem.swift
  12. 2
      PadelClub/Views/Subscription/StoreManager.swift
  13. 195
      PadelClub/Views/Subscription/SubscriptionView.swift
  14. 51
      PadelClub/Views/User/LoginView.swift
  15. 39
      PadelClub/Views/User/UserCreationView.swift

@ -15,6 +15,7 @@
C425D41C2B6D249E002A7B48 /* PadelClubUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C425D41B2B6D249E002A7B48 /* PadelClubUITests.swift */; };
C425D41E2B6D249E002A7B48 /* PadelClubUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C425D41D2B6D249E002A7B48 /* PadelClubUITestsLaunchTests.swift */; };
C44B79112BBDA63A00906534 /* Locale+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44B79102BBDA63A00906534 /* Locale+Extensions.swift */; };
C45BAE3B2BC6DF10002EEC8A /* SyncedProducts.storekit in Resources */ = {isa = PBXBuildFile; fileRef = C45BAE3A2BC6DF10002EEC8A /* SyncedProducts.storekit */; };
C4A47D5A2B6D383C00ADC637 /* Tournament.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D592B6D383C00ADC637 /* Tournament.swift */; };
C4A47D5E2B6D38EC00ADC637 /* DataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D5D2B6D38EC00ADC637 /* DataStore.swift */; };
C4A47D632B6D3D6500ADC637 /* Club.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D622B6D3D6500ADC637 /* Club.swift */; };
@ -263,6 +264,7 @@
C425D41D2B6D249E002A7B48 /* PadelClubUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PadelClubUITestsLaunchTests.swift; sourceTree = "<group>"; };
C425D44E2B6D24E1002A7B48 /* LeStorage.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = LeStorage.xcodeproj; path = ../../LeStorage/LeStorage.xcodeproj; sourceTree = "<group>"; };
C44B79102BBDA63A00906534 /* Locale+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Locale+Extensions.swift"; sourceTree = "<group>"; };
C45BAE3A2BC6DF10002EEC8A /* SyncedProducts.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = SyncedProducts.storekit; sourceTree = "<group>"; };
C4A47D592B6D383C00ADC637 /* Tournament.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tournament.swift; sourceTree = "<group>"; };
C4A47D5D2B6D38EC00ADC637 /* DataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStore.swift; sourceTree = "<group>"; };
C4A47D622B6D3D6500ADC637 /* Club.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Club.swift; sourceTree = "<group>"; };
@ -506,6 +508,7 @@
FFA6D78A2BB0BEB3003A31F3 /* Info.plist */,
C425D44E2B6D24E1002A7B48 /* LeStorage.xcodeproj */,
C425D4002B6D249D002A7B48 /* PadelClubApp.swift */,
C45BAE3A2BC6DF10002EEC8A /* SyncedProducts.storekit */,
FFD784002B91BF79000F62A6 /* Launch Screen.storyboard */,
C4A47D722B72881500ADC637 /* Views */,
FF3F74FD2B91A087004CFE0E /* ViewModel */,
@ -1152,6 +1155,7 @@
FF0EC5572BB195E20056B6D1 /* CLASSEMENT-PADEL-DAMES-10-2022.csv in Resources */,
FF0EC5582BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-3-05-2023.csv in Resources */,
FF0EC5592BB195E20056B6D1 /* CLASSEMENT-PADEL-DAMES-03-2023.csv in Resources */,
C45BAE3B2BC6DF10002EEC8A /* SyncedProducts.storekit in Resources */,
FF0EC55A2BB195E20056B6D1 /* CLASSEMENT PADEL MESSIEURS_2-07-2023.csv in Resources */,
FF0EC55B2BB195E20056B6D1 /* CLASSEMENT-PADEL-DAMES-01-2023.csv in Resources */,
FF0EC55C2BB195E20056B6D1 /* CLASSEMENT PADEL DAMES-08-2023.csv in Resources */,
@ -1516,7 +1520,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = 526E96RFNP;
DEVELOPMENT_TEAM = BQ3Y44M3Q6;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = PadelClub/Info.plist;
@ -1531,7 +1535,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.staxriver.PadelClub;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
@ -1547,7 +1551,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = 526E96RFNP;
DEVELOPMENT_TEAM = BQ3Y44M3Q6;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = PadelClub/Info.plist;
@ -1562,7 +1566,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.staxriver.PadelClub;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;

@ -0,0 +1,104 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1520"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C425D3FC2B6D249D002A7B48"
BuildableName = "PadelClub.app"
BlueprintName = "PadelClub"
ReferencedContainer = "container:PadelClub.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C425D40C2B6D249E002A7B48"
BuildableName = "PadelClubTests.xctest"
BlueprintName = "PadelClubTests"
ReferencedContainer = "container:PadelClub.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C425D4162B6D249E002A7B48"
BuildableName = "PadelClubUITests.xctest"
BlueprintName = "PadelClubUITests"
ReferencedContainer = "container:PadelClub.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C425D3FC2B6D249D002A7B48"
BuildableName = "PadelClub.app"
BlueprintName = "PadelClub"
ReferencedContainer = "container:PadelClub.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<StoreKitConfigurationFileReference
identifier = "../../PadelClub/SyncedProducts.storekit">
</StoreKitConfigurationFileReference>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C425D3FC2B6D249D002A7B48"
BuildableName = "PadelClub.app"
BlueprintName = "PadelClub"
ReferencedContainer = "container:PadelClub.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

@ -0,0 +1,56 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.000",
"green" : "0.573",
"red" : "0.953"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "light"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.000",
"green" : "0.573",
"red" : "0.953"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.000",
"green" : "0.827",
"red" : "0.996"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

@ -37,8 +37,8 @@ class DataStore: ObservableObject {
init() {
let store = Store.main
// store.synchronizationApiURL = "http://127.0.0.1:8000/api/"
store.synchronizationApiURL = "https://xlr.alwaysdata.net/api/"
store.synchronizationApiURL = "http://127.0.0.1:8000/api/"
// store.synchronizationApiURL = "https://xlr.alwaysdata.net/api/"
// store.addMigration(Migration<ClubV1, Club>(version: 2))
// store.addMigration(Migration<TournamentV1, TournamentV2>(version: 2))

@ -5,10 +5,10 @@
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleTypeName</key>
<string>CSV,XLS</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSHandlerRank</key>
<string>Alternate</string>
<key>LSItemContentTypes</key>

@ -18,7 +18,7 @@ struct PadelClubApp: App {
WindowGroup {
MainView()
.environment(navigationViewModel)
.accentColor(.launchScreenBackground)
.accentColor(.master)
.onAppear {
self._onAppear()
}

@ -0,0 +1,118 @@
{
"identifier" : "2055C391",
"nonRenewingSubscriptions" : [
],
"products" : [
{
"displayPrice" : "14.0",
"familyShareable" : false,
"internalID" : "6484163993",
"localizations" : [
{
"description" : "Achetez votre tournoi à l'unité",
"displayName" : "Tournoi à l'unité",
"locale" : "fr"
}
],
"productID" : "app.padelclub.tournament.unit",
"referenceName" : "Tournoi à l'unité",
"type" : "Consumable"
}
],
"settings" : {
"_applicationInternalID" : "6484163558",
"_developerTeamID" : "BQ3Y44M3Q6",
"_failTransactionsEnabled" : false,
"_lastSynchronizedDate" : 734533081.06639695,
"_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" : "21474782",
"localizations" : [
],
"name" : "Main",
"subscriptions" : [
{
"adHocOffers" : [
],
"codeOffers" : [
],
"displayPrice" : "89.0",
"familyShareable" : false,
"groupNumber" : 1,
"internalID" : "6484163670",
"introductoryOffer" : null,
"localizations" : [
{
"description" : "Créez autant de tournois que vous souhaitez",
"displayName" : "Abonnement illimité",
"locale" : "fr"
}
],
"productID" : "app.padelclub.unlimited",
"recurringSubscriptionPeriod" : "P1M",
"referenceName" : "Unlimited",
"subscriptionGroupID" : "21474782",
"type" : "RecurringSubscription"
}
]
}
],
"version" : {
"major" : 3,
"minor" : 0
}
}

@ -19,7 +19,7 @@ struct StepperView: View {
var body: some View {
VStack(spacing: 0) {
HStack(spacing: 16) {
HStack(spacing: 8) {
Button(action: {
self._subtract()
}, label: {
@ -34,8 +34,9 @@ struct StepperView: View {
TextField("00", value: $count, format: .number)
.keyboardType(.numberPad)
.fixedSize()
.font(.title2)
// .font(.title2)
.monospacedDigit()
.multilineTextAlignment(.center)
.onSubmit {
if let minimum, count < minimum {
count = minimum

@ -31,7 +31,7 @@ struct SelectablePlayerListView: View {
init(allowSelection: Int = 0, searchField: String? = nil, user: User? = nil, dataSet: DataSet = .national, filterOption: PlayerFilterOption = .all, hideAssimilation: Bool = false, ascending: Bool = true, sortOption: SortOption = .rank, fromPlayer: FederalPlayer? = nil, codeClub: String? = nil, ligue: String? = nil, playerSelectionAction: PlayerSelectionAction? = nil, contentUnavailableAction: ContentUnavailableAction? = nil) {
self.allowSelection = allowSelection
self.searchText = searchField ?? ""
// self.searchText = searchField ?? ""
self.playerSelectionAction = playerSelectionAction
self.contentUnavailableAction = contentUnavailableAction
let searchViewModel = SearchViewModel()

@ -106,7 +106,7 @@ import LeStorage
var currentPlan: StoreItem? {
#if DEBUG
return .monthly
return .unlimited
#else
if let currentBestPlan = self.currentBestPlan, let plan = StorePlan(rawValue: currentBestPlan.productID) {
return plan
@ -122,7 +122,7 @@ import LeStorage
fileprivate func _updateBestPlan() {
if let monthly = self.purchasedTransactions.first(where: { $0.productID == StoreItem.monthly.rawValue }) {
if let monthly = self.purchasedTransactions.first(where: { $0.productID == StoreItem.unlimited.rawValue }) {
self.currentBestPlan = monthly
} else {
self.currentBestPlan = nil

@ -18,5 +18,19 @@ enum StoreItem: String, Identifiable, CaseIterable {
// var formattedPrice: String { return "119.99 / an" }
//
// var price: Double { return 19.99 }
var systemImage: String {
switch self {
case .unlimited: return "star.circle.fill"
case .unit: return "tennisball.circle.fill"
}
}
var isConsumable: Bool {
switch self {
case .unlimited: return false
case .unit: return true
}
}
}

@ -56,6 +56,8 @@ class StoreManager {
func requestProducts() async {
do {
let identifiers: [String] = StoreItem.allCases.map { $0.rawValue }
Logger.log("Request products: \(identifiers)")
var products = try await Product.products(for: identifiers)
products = products.sorted { p1, p2 in
return p2.price > p1.price

@ -7,6 +7,30 @@
import SwiftUI
import StoreKit
import LeStorage
extension Product.SubscriptionPeriod.Unit {
var label: String {
switch self {
case .day: return "jour"
case .week: return "semaine"
case .month: return "mois"
case .year: return "année"
@unknown default: return "inconnu"
}
}
}
extension Product {
var item: StoreItem {
return StoreItem(rawValue: self.id)!
}
var formattedPrice: String {
if let period = self.subscription?.subscriptionPeriod {
return self.displayPrice + " / " + period.unit.label
}
return self.displayPrice
}
}
class SubscriptionModel: ObservableObject, StoreDelegate {
@ -14,9 +38,18 @@ class SubscriptionModel: ObservableObject, StoreDelegate {
@Published var error: Error? = nil
@Published var isLoading: Bool = false
@Published var selectedProduct: Product? = nil
@Published var quantity: Int = 1
@Published var selectedProduct: Product? = nil {
didSet {
self._computePrice()
}
}
@Published var quantity: Int = 1 {
didSet {
self._computePrice()
}
}
@Published var products: [Product] = []
@Published var totalPrice: String = ""
func load() {
self.isLoading = true
@ -34,46 +67,110 @@ class SubscriptionModel: ObservableObject, StoreDelegate {
}
func purchase() {
if let product = self.selectedProduct {
Task {
guard let product: Product = self.selectedProduct else {
return
}
Task {
if product.item.isConsumable {
let _ = try await self.storeManager?.purchase(product, quantity: self.quantity)
} else {
let _ = try await self.storeManager?.purchase(product)
}
}
}
fileprivate func _computePrice() {
if let product = self.selectedProduct, let item = StoreItem(rawValue: product.id) {
if item.isConsumable {
let price = NSDecimalNumber(decimal: product.price).multiplying(by: NSDecimalNumber(integerLiteral: self.quantity))
self.totalPrice = product.priceFormatStyle.format(price.decimalValue)
} else {
self.totalPrice = product.displayPrice
}
} else {
self.totalPrice = ""
}
}
}
struct SubscriptionView: View {
@ObservedObject var model: SubscriptionModel = SubscriptionModel()
@State var isRestoring: Bool = false
var body: some View {
Form {
List {
ForEach(self.model.products) { product in
ProductView(product: product,
quantity: self.$model.quantity)
.onTapGesture {
self.model.selectedProduct = product
VStack {
if self.model.products.count > 0 {
Form {
List {
ForEach(self.model.products) { product in
ProductView(product: product,
quantity: self.$model.quantity, selected: self.model.selectedProduct == product)
.onTapGesture {
self.model.selectedProduct = product
}
}
}
Section {
Button {
self._purchase()
} label: {
HStack {
Text("Purchase")
if let _ = self.model.selectedProduct {
Spacer()
Text(self.model.totalPrice)
}
}
}
} footer : {
if self.model.selectedProduct?.item.isConsumable == false {
Text("Conditions d’utilisations concernant l’abonnement:\n- Le paiement sera facturé sur votre compte Apple.\n- L’abonnement est renouvelé automatiquement chaque mois, à moins d’avoir été désactivé au moins 24 heures avant la fin de la période de l’abonnement.\n- L’abonnement peut être géré par l’utilisateur et désactivé en allant dans les réglages de son compte après s’être abonné.\n- Le compte sera facturé pour le renouvellement de l'abonnement dans les 24 heures précédent la fin de la période d’abonnement.\n- Un abonnement en cours ne peut être annulé.\n- Toute partie inutilisée de l'offre gratuite, si souscrite, sera abandonnée lorsque l'utilisateur s'abonnera, dans les cas applicables.")
}
}
}
} else {
VStack(alignment: .center) {
if let error = self.model.error {
Text(error.localizedDescription)
} else {
Text("Aucun produit disponible")
}
if self.model.isLoading {
ProgressView()
} else {
Button(action: {
self._load()
}, label: {
Image(systemName: "arrow.clockwise.circle.fill")
.font(.system(size: 64.0))
})
}
}
}
Section {
Button {
self._purchase()
} label: {
VStack {
Text("Purchase")
if let product = self.model.selectedProduct {
Text(product.displayName)
Text(product.displayPrice)
}
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
if self.isRestoring {
ProgressView()
} else {
Button("Restore") {
self._restore()
}
}
}
}
.navigationTitle("Subscriptions")
.onAppear {
@ -86,7 +183,23 @@ struct SubscriptionView: View {
}
fileprivate func _load() {
self.model.load()
}
fileprivate func _restore() {
Task {
do {
self.isRestoring = true
try await Guard.main.refreshPurchasedProducts()
self.isRestoring = false
} catch {
self.isRestoring = false
Logger.error(error)
}
}
}
}
@ -95,18 +208,44 @@ struct ProductView: View {
var product: Product
@Binding var quantity: Int
var selected: Bool
var body: some View {
HStack {
Image(systemName: "star.circle.fill")
Image(systemName: self._image)
.font(.title)
.foregroundColor(.blue)
.foregroundColor(.accentColor)
VStack(alignment: .leading) {
Text(product.displayName)
Text(product.displayPrice).foregroundColor(.blue)
Text(product.formattedPrice)
.foregroundColor(.accentColor)
if self._isConsumable {
StepperView(count: self.$quantity, minimum: 1).font(.callout)
// Stepper(value: self.$quantity) {
// Text("")
// }
}
}
Spacer()
Image(systemName: "checkmark").foregroundColor(.blue)
if self.selected {
Image(systemName: "checkmark").foregroundColor(.accentColor)
}
}.contentShape(.rect)
}
fileprivate var _item: StoreItem? {
return StoreItem(rawValue: self.product.id)
}
fileprivate var _isConsumable: Bool {
return self._item?.isConsumable ?? false
}
fileprivate var _image: String {
if let item = self._item {
return item.systemImage
} else {
return "gift.circle.fill"
}
}

@ -19,16 +19,25 @@ struct LoginView: View {
@State var error: Error? = nil
var showEmailValidationMessage: Bool = false
var handler: (User) -> ()
var body: some View {
Form {
TextField("Username", text: self.$username)
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
SecureField("Password", text: self.$password)
Section {
TextField("Username", text: self.$username)
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
SecureField("Password", text: self.$password)
} header: {
if self.showEmailValidationMessage {
Text("Vous pouvez maintenant ouvrir votre boîte mail pour valider votre compte. Vous pourrez ensuite vous connecter ici.")
}
}
Section {
Button(action: {
@ -39,22 +48,24 @@ struct LoginView: View {
.frame(maxWidth: .infinity)
}
Section {
NavigationLink("Sign up") {
UserCreationView()
}
Button(action: {
self.showEmailPopup = true
}, label: {
Text("Forgotten password")
})
.alert(
Text("Password reset"),
isPresented: self.$showEmailPopup
) {
EmailConfirmationView()
} message: {
Text("Please enter your email")
if !self.showEmailValidationMessage {
Section {
NavigationLink("Sign up") {
UserCreationView()
}
Button(action: {
self.showEmailPopup = true
}, label: {
Text("Forgotten password")
})
.alert(
Text("Password reset"),
isPresented: self.$showEmailPopup
) {
EmailConfirmationView()
} message: {
Text("Please enter your email")
}
}
}

@ -8,8 +8,10 @@
import SwiftUI
import LeStorage
struct UserCreationView: View {
struct UserCreationFormView: View {
@Binding var showLoginScreen: Bool
@State var username: String = ""
@State var password1: String = ""
@State var password2: String = ""
@ -21,11 +23,12 @@ struct UserCreationView: View {
@State var showUnmatchingPasswordView = false
@State var selectedCountryIndex = 0
@State var dataCollectAuthorized: Bool = false
let countries: [String] = Locale.countries()
@State var isLoading = false
@State var showLoginScreen: Bool = false
var body: some View {
@ -62,6 +65,12 @@ struct UserCreationView: View {
.padding()
}
Section {
Toggle(isOn: self.$dataCollectAuthorized) {
Text("J'autorise XLR Sport, éditeur de Padel Club, à sauvegarder les données ci-dessus. XLR Sport s'engage à ne pas partager ces données.").font(.footnote)
}
}
Section {
Button(action: {
self._create()
@ -71,7 +80,7 @@ struct UserCreationView: View {
} else {
Text("Create")
}
})
}).disabled(!self.dataCollectAuthorized)
.frame(maxWidth: .infinity)
}
}
@ -81,9 +90,9 @@ struct UserCreationView: View {
.alert("Password do not match", isPresented: self.$showUnmatchingPasswordView, actions: {
Button("Ok", action: {})
} )
.navigationTitle("Create user")
}
fileprivate func _selectCountry() {
guard let regionCode = Locale.current.region?.identifier, let country = Locale.current.localizedString(forRegionCode: regionCode) else {
return
@ -115,17 +124,35 @@ struct UserCreationView: View {
email: self.email,
phone: self.phone,
country: self.countries[self.selectedCountryIndex])
let _: User = try await service.createAccount(user: userCreationForm)
self.isLoading = false
self.showLoginScreen = true
} catch {
self.isLoading = false
Logger.error(error)
}
}
}
}
struct UserCreationView: View {
@State var showLoginScreen: Bool = false
var body: some View {
Group {
if self.showLoginScreen {
LoginView(showEmailValidationMessage: true) { _ in }
} else {
UserCreationFormView(showLoginScreen: self.$showLoginScreen)
}
}
.navigationTitle("Create user")
}
}

Loading…
Cancel
Save