diff --git a/Packages/BeMatch/Sources/MembershipFeature/Assets.xcassets/bematch-campaign.imageset/Contents.json b/Packages/BeMatch/Sources/MembershipFeature/Assets.xcassets/bematch-campaign.imageset/Contents.json new file mode 100644 index 00000000..97c4b195 --- /dev/null +++ b/Packages/BeMatch/Sources/MembershipFeature/Assets.xcassets/bematch-campaign.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "bematch-campaign.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Packages/BeMatch/Sources/MembershipFeature/Assets.xcassets/bematch-campaign.imageset/bematch-campaign.pdf b/Packages/BeMatch/Sources/MembershipFeature/Assets.xcassets/bematch-campaign.imageset/bematch-campaign.pdf new file mode 100644 index 00000000..090200f0 Binary files /dev/null and b/Packages/BeMatch/Sources/MembershipFeature/Assets.xcassets/bematch-campaign.imageset/bematch-campaign.pdf differ diff --git a/Packages/BeMatch/Sources/MembershipFeature/Assets.xcassets/get-benefit.imageset/Contents.json b/Packages/BeMatch/Sources/MembershipFeature/Assets.xcassets/get-benefit.imageset/Contents.json new file mode 100644 index 00000000..774b7105 --- /dev/null +++ b/Packages/BeMatch/Sources/MembershipFeature/Assets.xcassets/get-benefit.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "get-benefit.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Packages/BeMatch/Sources/MembershipFeature/Assets.xcassets/get-benefit.imageset/get-benefit.pdf b/Packages/BeMatch/Sources/MembershipFeature/Assets.xcassets/get-benefit.imageset/get-benefit.pdf new file mode 100644 index 00000000..8a7cc7cb Binary files /dev/null and b/Packages/BeMatch/Sources/MembershipFeature/Assets.xcassets/get-benefit.imageset/get-benefit.pdf differ diff --git a/Packages/BeMatch/Sources/MembershipFeature/Assets.xcassets/invite-ticket.imageset/Contents.json b/Packages/BeMatch/Sources/MembershipFeature/Assets.xcassets/invite-ticket.imageset/Contents.json new file mode 100644 index 00000000..cc7dedfb --- /dev/null +++ b/Packages/BeMatch/Sources/MembershipFeature/Assets.xcassets/invite-ticket.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "invite-ticket.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Packages/BeMatch/Sources/MembershipFeature/Assets.xcassets/invite-ticket.imageset/invite-ticket.pdf b/Packages/BeMatch/Sources/MembershipFeature/Assets.xcassets/invite-ticket.imageset/invite-ticket.pdf new file mode 100644 index 00000000..c8ae6045 Binary files /dev/null and b/Packages/BeMatch/Sources/MembershipFeature/Assets.xcassets/invite-ticket.imageset/invite-ticket.pdf differ diff --git a/Packages/BeMatch/Sources/MembershipFeature/ButtonStyle/ConversionPrimaryButtonStyle.swift b/Packages/BeMatch/Sources/MembershipFeature/ButtonStyle/ConversionPrimaryButtonStyle.swift new file mode 100644 index 00000000..9d9290da --- /dev/null +++ b/Packages/BeMatch/Sources/MembershipFeature/ButtonStyle/ConversionPrimaryButtonStyle.swift @@ -0,0 +1,25 @@ +import Styleguide +import SwiftUI + +struct ConversionPrimaryButtonStyle: ButtonStyle { + func makeBody(configuration: Configuration) -> some View { + configuration.label + .font(.system(.subheadline, weight: .semibold)) + .frame(height: 50) + .frame(maxWidth: .infinity) + .foregroundStyle(Color.black) + .background( + LinearGradient( + colors: [ + Color(0xFFE8_B423), + Color(0xFFF5_D068), + ], + startPoint: .leading, + endPoint: .trailing + ) + ) + .clipShape(RoundedRectangle(cornerRadius: 16)) + .scaleEffect(configuration.isPressed ? 0.9 : 1.0) + .animation(.default, value: configuration.isPressed) + } +} diff --git a/Packages/BeMatch/Sources/MembershipFeature/ButtonStyle/ConversionSecondaryButtonStyle.swift b/Packages/BeMatch/Sources/MembershipFeature/ButtonStyle/ConversionSecondaryButtonStyle.swift new file mode 100644 index 00000000..10dff7bb --- /dev/null +++ b/Packages/BeMatch/Sources/MembershipFeature/ButtonStyle/ConversionSecondaryButtonStyle.swift @@ -0,0 +1,16 @@ +import Styleguide +import SwiftUI + +struct ConversionSecondaryButtonStyle: ButtonStyle { + func makeBody(configuration: Configuration) -> some View { + configuration.label + .font(.system(.subheadline, weight: .semibold)) + .frame(height: 50) + .frame(maxWidth: .infinity) + .foregroundStyle(Color.black) + .background(Color.gray) + .clipShape(RoundedRectangle(cornerRadius: 16)) + .scaleEffect(configuration.isPressed ? 0.9 : 1.0) + .animation(.default, value: configuration.isPressed) + } +} diff --git a/Packages/BeMatch/Sources/MembershipFeature/InvitationCampaign.swift b/Packages/BeMatch/Sources/MembershipFeature/InvitationCampaign.swift index 015c543b..cbcc2e45 100644 --- a/Packages/BeMatch/Sources/MembershipFeature/InvitationCampaign.swift +++ b/Packages/BeMatch/Sources/MembershipFeature/InvitationCampaign.swift @@ -8,7 +8,11 @@ public struct InvitationCampaignLogic { public init() {} public struct State: Equatable { - public init() {} + let quantity: Int + + public init(quantity: Int) { + self.quantity = quantity + } } public enum Action { @@ -62,9 +66,9 @@ public struct InvitationCampaignView: View { } public var body: some View { - WithViewStore(store, observe: { $0 }) { _ in + WithViewStore(store, observe: { $0 }) { viewStore in VStack(spacing: 12) { - Text("Limited to the first 2,000 Users", bundle: .module) + Text("Limited to the first \(viewStore.quantity) Users", bundle: .module) .font(.system(.headline, weight: .semibold)) .padding(.vertical, 6) .padding(.horizontal, 8) @@ -100,7 +104,9 @@ public struct InvitationCampaignView: View { #Preview { InvitationCampaignView( store: .init( - initialState: InvitationCampaignLogic.State(), + initialState: InvitationCampaignLogic.State( + quantity: 2000 + ), reducer: { InvitationCampaignLogic() } ) ) diff --git a/Packages/BeMatch/Sources/MembershipFeature/InvitationCodeCampaign.swift b/Packages/BeMatch/Sources/MembershipFeature/InvitationCodeCampaign.swift index f08e6bad..9acc5f38 100644 --- a/Packages/BeMatch/Sources/MembershipFeature/InvitationCodeCampaign.swift +++ b/Packages/BeMatch/Sources/MembershipFeature/InvitationCodeCampaign.swift @@ -8,6 +8,7 @@ public struct InvitationCodeCampaignLogic { public init() {} public struct State: Equatable { + var code = "ACF" public init() {} } @@ -40,14 +41,25 @@ public struct InvitationCodeCampaignView: View { } public var body: some View { - WithViewStore(store, observe: { $0 }) { _ in + WithViewStore(store, observe: { $0 }) { viewStore in VStack(spacing: 16) { - Text("When a friend enters your invitation code\n500 yen per week when you enter the code", bundle: .module) - .font(.body) + Image(ImageResource.bematchCampaign) + .resizable() + .padding(.horizontal, 60) VStack(spacing: 16) { + Image(ImageResource.inviteTicket) + .resizable() + .aspectRatio(contentMode: .fit) + .overlay(alignment: .center) { + Text(viewStore.code) + .foregroundStyle(Color(0xFFFF_CC00)) + .font(.system(.largeTitle, design: .rounded, weight: .bold)) + .offset(x: -35, y: 8) + } + PrimaryButton( - String(localized: "Share Invitation Code", bundle: .module) + String(localized: "Send Invitation Code", bundle: .module) ) {} } .padding(.all, 16) @@ -57,6 +69,7 @@ public struct InvitationCodeCampaignView: View { } .padding(.vertical, 24) .background() + .multilineTextAlignment(.center) .task { await store.send(.onTask).finish() } .onAppear { store.send(.onAppear) } } diff --git a/Packages/BeMatch/Sources/MembershipFeature/Localizable.xcstrings b/Packages/BeMatch/Sources/MembershipFeature/Localizable.xcstrings index fbbcdfba..c570c004 100644 --- a/Packages/BeMatch/Sources/MembershipFeature/Localizable.xcstrings +++ b/Packages/BeMatch/Sources/MembershipFeature/Localizable.xcstrings @@ -24,12 +24,12 @@ } } }, - "Limited to the first 2,000 Users" : { + "Limited to the first %lld Users" : { "localizations" : { "ja" : { "stringUnit" : { "state" : "translated", - "value" : "先着2,000名限定" + "value" : "先着%lld名限定" } } } @@ -57,22 +57,22 @@ } } }, - "Share Invitation Code" : { + "Send Invitation Code" : { "localizations" : { "ja" : { "stringUnit" : { "state" : "translated", - "value" : "招待コードをシェアする" + "value" : "招待コードを送る" } } } }, - "When a friend enters your invitation code\n500 yen per week when you enter the code" : { + "Upgrade for ¥500/week" : { "localizations" : { "ja" : { "stringUnit" : { "state" : "translated", - "value" : "友達があなたの招待コードを\n入力すると、週500円の" + "value" : "¥500円/週でアップグレードする" } } } diff --git a/Packages/BeMatch/Sources/MembershipFeature/Membership.swift b/Packages/BeMatch/Sources/MembershipFeature/Membership.swift index 0bf053fd..19f5b53c 100644 --- a/Packages/BeMatch/Sources/MembershipFeature/Membership.swift +++ b/Packages/BeMatch/Sources/MembershipFeature/Membership.swift @@ -10,12 +10,13 @@ public struct MembershipLogic { public struct State: Equatable { var child: Child.State? + var isActivityIndicatorVisible = false + public init() {} } public enum Action { case onTask - case onAppear case closeButtonTapped case activeInvitationCampaignResponse(Result) case child(Child.Action) @@ -42,10 +43,6 @@ public struct MembershipLogic { } .cancellable(id: Cancel.activeInvitationCampaign, cancelInFlight: true) - case .onAppear: - analytics.logScreen(screenName: "Membership", of: self) - return .none - case .closeButtonTapped: return .run { _ in await dismiss() @@ -103,37 +100,47 @@ public struct MembershipView: View { } public var body: some View { - IfLetStore(store.scope(state: \.child, action: \.child)) { store in - SwitchStore(store) { initialState in - switch initialState { - case .campaign: - CaseLet( - /MembershipLogic.Child.State.campaign, - action: MembershipLogic.Child.Action.campaign, - then: MembershipCampaignView.init(store:) - ) - case .purchase: - CaseLet( - /MembershipLogic.Child.State.purchase, - action: MembershipLogic.Child.Action.purchase, - then: MembershipPurchaseView.init(store:) - ) + WithViewStore(store, observe: { $0 }) { viewStore in + IfLetStore(store.scope(state: \.child, action: \.child)) { store in + SwitchStore(store) { initialState in + switch initialState { + case .campaign: + CaseLet( + /MembershipLogic.Child.State.campaign, + action: MembershipLogic.Child.Action.campaign, + then: MembershipCampaignView.init(store:) + ) + case .purchase: + CaseLet( + /MembershipLogic.Child.State.purchase, + action: MembershipLogic.Child.Action.purchase, + then: MembershipPurchaseView.init(store:) + ) + } } + } else: { + ProgressView() + .tint(Color.primary) } - } else: { - ProgressView() - .tint(Color.primary) - } - .ignoresSafeArea() - .task { await store.send(.onTask).finish() } - .onAppear { store.send(.onAppear) } - .toolbar { - ToolbarItem(placement: .topBarLeading) { - Button { - store.send(.closeButtonTapped) - } label: { - Image(systemName: "xmark") - .foregroundStyle(Color.primary) + .ignoresSafeArea() + .task { await store.send(.onTask).finish() } + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button { + store.send(.closeButtonTapped) + } label: { + Image(systemName: "xmark") + .foregroundStyle(Color.primary) + } + } + } + .overlay { + if viewStore.isActivityIndicatorVisible { + ProgressView() + .tint(Color.white) + .progressViewStyle(CircularProgressViewStyle()) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.black.opacity(0.6)) } } } diff --git a/Packages/BeMatch/Sources/MembershipFeature/MembershipCampaign.swift b/Packages/BeMatch/Sources/MembershipFeature/MembershipCampaign.swift index dd333eee..02bca09f 100644 --- a/Packages/BeMatch/Sources/MembershipFeature/MembershipCampaign.swift +++ b/Packages/BeMatch/Sources/MembershipFeature/MembershipCampaign.swift @@ -10,11 +10,12 @@ public struct MembershipCampaignLogic { public struct State: Equatable { let campaign: BeMatch.ActiveInvitationCampaignQuery.Data.ActiveInvitationCampaign - var invitationCampaign = InvitationCampaignLogic.State() + var invitationCampaign: InvitationCampaignLogic.State var invitationCodeCampaign = InvitationCodeCampaignLogic.State() public init(campaign: BeMatch.ActiveInvitationCampaignQuery.Data.ActiveInvitationCampaign) { self.campaign = campaign + self.invitationCampaign = InvitationCampaignLogic.State(quantity: campaign.quantity) } } @@ -54,27 +55,76 @@ public struct MembershipCampaignView: View { public var body: some View { WithViewStore(store, observe: { $0 }) { _ in - ScrollView { - VStack(spacing: 0) { - InvitationCampaignView( - store: store.scope( - state: \.invitationCampaign, - action: \.invitationCampaign + VStack(spacing: 16) { + ScrollView { + VStack(spacing: 0) { + InvitationCampaignView( + store: store.scope( + state: \.invitationCampaign, + action: \.invitationCampaign + ) ) - ) - InvitationCodeCampaignView( - store: store.scope( - state: \.invitationCodeCampaign, - action: \.invitationCodeCampaign + InvitationCodeCampaignView( + store: store.scope( + state: \.invitationCodeCampaign, + action: \.invitationCodeCampaign + ) ) - ) - - PurchaseAboutView() + + VStack(spacing: 60) { + Image(ImageResource.membershipBenefit) + .resizable() + + PurchaseAboutView() + } + .padding(.horizontal, 16) + } + .padding(.bottom, 80) + } + + VStack(spacing: 16) { + Button { + + } label: { + Text("Send Invitation Code", bundle: .module) + } + .buttonStyle(ConversionPrimaryButtonStyle()) + + Button { + + } label: { + Text("Upgrade for ¥500/week", bundle: .module) + } + .buttonStyle(ConversionSecondaryButtonStyle()) } + .padding(.horizontal, 16) + .padding(.bottom, 36) } .background() .task { await store.send(.onTask).finish() } } } } + +#Preview { + MembershipCampaignView( + store: .init( + initialState: MembershipCampaignLogic.State( + campaign: BeMatch.ActiveInvitationCampaignQuery.Data.ActiveInvitationCampaign( + _dataDict: DataDict( + data: [ + "id":"1", + "quantity": 2000, + ], + fulfilledFragments: [] + ) + ) + ), + reducer: { MembershipCampaignLogic() } + ) + ) + .ignoresSafeArea() + .environment(\.colorScheme, .dark) + .environment(\.locale, Locale(identifier: "ja-JP")) +} diff --git a/Packages/BeMatch/Sources/MembershipFeature/MembershipPurchase.swift b/Packages/BeMatch/Sources/MembershipFeature/MembershipPurchase.swift index 9dc65d8c..22fc2a92 100644 --- a/Packages/BeMatch/Sources/MembershipFeature/MembershipPurchase.swift +++ b/Packages/BeMatch/Sources/MembershipFeature/MembershipPurchase.swift @@ -52,7 +52,7 @@ public struct MembershipPurchaseView: View { public var body: some View { WithViewStore(store, observe: { $0 }) { _ in - VStack(spacing: 8) { + VStack(spacing: 16) { ScrollView { VStack(spacing: 24) { Image(ImageResource.purchaseHeader)