Skip to content

Commit

Permalink
Add embedded payment form (#7143)
Browse files Browse the repository at this point in the history
* Add some scaffolding

Make js pack more robust

Add checkout element

WIP

Update things for prices

Add return js for confirmed page

Add correct endpoint

Sort things out

Use stripe publishable-key's meta tag as a key

Add meta tag

* Continue to add stuff

* Copy

* Update final copy

* Break out partial

* Break out partial

* Update pricing data

* Update the user status when payment is completed

* Pay Responsive tweaks (#7144)

* Basics

* Refine layout

* Make more things look cool

---------

Co-authored-by: Aron Demeter <[email protected]>
  • Loading branch information
iHiD and dem4ron authored Nov 7, 2024
1 parent 913b4f8 commit 9293859
Show file tree
Hide file tree
Showing 21 changed files with 848 additions and 221 deletions.
47 changes: 43 additions & 4 deletions app/controllers/bootcamp_controller.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
class BootcampController < ApplicationController
layout "bootcamp"
layout 'bootcamp'

skip_before_action :authenticate_user!
before_action :use_user_bootcamp_data!
Expand Down Expand Up @@ -46,13 +46,52 @@ def do_enrollment
ppp_country: @country_code_2
)

redirect_to action: :enrollment_confirmed, package: params[:package]
redirect_to action: :pay
end

def enrollment_confirmed
@price = params[:package] == "complete" ? @complete_price : @part_1_price
def pay; end

def stripe_create_checkout_session
# Sample for you to use. This will work with Stripe's test cards.
if Rails.env.production?
stripe_price = @bootcamp_data.stripe_price_id
else
stripe_price = "price_1QCjUFEoOT0Jqx0UJOkhigru"
end

session = Stripe::Checkout::Session.create({
ui_mode: 'embedded',
customer_email: @bootcamp_data.email,
line_items: [{
price: stripe_price,
quantity: 1
}],
mode: 'payment',
allow_promotion_codes: true,
return_url: "#{bootcamp_confirmed_url}?session_id={CHECKOUT_SESSION_ID}"
})

render json: { clientSecret: session.client_secret }
end

def stripe_session_status
session = Stripe::Checkout::Session.retrieve(params[:session_id])

if session.status == 'complete'
@bootcamp_data.update!(
paid_at: Time.current,
payment_intent_id: session.id
)
end

render json: {
status: session.status,
customer_email: session.customer_details.email
}
end

def confirmed; end

private
def use_user_bootcamp_data!
user_id = cookies.signed[:_exercism_user_id]
Expand Down
126 changes: 112 additions & 14 deletions app/css/packs/bootcamp.css
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ section#bootcamp {
@apply text-[12px] font-semibold;
@apply uppercase text-[#7029f5];
@apply mb-16;
@apply mx-auto py-4 px-12 rounded-[12px];
@apply mx-auto py-4 px-12 rounded-[12px];
@apply bg-[#7029f522];
}
h2 {
Expand Down Expand Up @@ -992,7 +992,9 @@ section#legals {
}
}

section#enroll {
section#enroll,
section#payment,
section#confirmation {
@apply min-h-[100vh];
@apply pt-64 pb-80;
background-color: #7029f5;
Expand Down Expand Up @@ -1021,12 +1023,17 @@ section#enroll {
}
}

hr {
@apply border-b-1 border-[#eee] my-20;
width: 100%;
}

.container {
border: 1px solid #eee;
box-shadow: 0 0 30px rgba(0, 0, 0, 0.4);
background: white;
@apply md:pt-32 md:pb-32 pt-16 pb-8 md:px-48 px-24 rounded-[8px];
@apply flex flex-col items-center;
@apply flex flex-col;
}

.tag {
Expand All @@ -1043,6 +1050,12 @@ section#enroll {
@apply font-semibold;
}
}
p.intro {
@apply md:text-22 text-18 font-light text-left;
strong {
@apply font-semibold;
}
}

p.info {
@apply md:text-18 text-16 font-light;
Expand All @@ -1053,19 +1066,15 @@ section#enroll {
@apply font-medium;
}
}
}

.intro {
@apply md:text-22 text-18 font-light;
text-align: center;
text-wrap: balance;
@apply max-w-[750px] mx-auto;
strong {
@apply font-semibold;
}
section#enroll {
.container {
@apply items-center;
}
hr {
@apply border-b-1 border-[#eee] my-20;
width: 100%;

p.intro {
@apply mx-auto text-center;
}

form {
Expand Down Expand Up @@ -1215,6 +1224,95 @@ section#enroll {
color: rgb(255 255 255);
}
}
section#payment {
.lg-container {
@apply md:px-[3%] px-8;
.container {
align-items: center;
@apply lg:items-start items-center;
@apply flex lg:flex-row flex-col gap-40;
@apply md:p-24 py-16 px-12;
}
}
.tag {
@apply mx-0;
}

.lhs {
@apply flex flex-col items-start;
@apply pt-20 mt-[-20px];
@apply lg:sticky top-0;
h3 {
@apply text-22 mb-8;
}
}
li {
background-image: url("icons/checkmark-neon.svg");
background-size: 14px;
@apply bg-no-repeat;
@apply pl-20 mb-6;
background-position: 0px 4px;
strong {
@apply font-semibold;
}
}
p.intro {
@apply lg:text-20 md:text-18;
@apply text-pretty;
}
p.info {
@apply text-left text-pretty;
}

.rhs {
@apply lg:w-[410px] w-[100%] flex-shrink-0;
#checkout {
@apply w-fill;
background: rgb(96, 79, 205);
@apply p-12 rounded-8;
box-shadow: 0 0 10px rgba(96, 79, 205, 0.5);
}
}
.guarantee {
@apply border-1 rounded-8;
@apply py-4 px-4;
@apply sm:mt-20 mt-32;
border-color: rgb(63, 203, 134);
background-color: rgb(244, 255, 250);
h3 {
@apply text-18;
color: rgb(0, 120, 50);
}
p {
color: rgb(0, 120, 50);
}
}
}

section#confirmation {
.container {
@apply items-start;
@apply flex flex-col;
}
.tag {
@apply mx-0 mb-2;
}
p.intro {
@apply text-pretty;
}
p.info {
@apply text-left text-pretty;
}
p.info + p.info {
@apply mt-12;
}
.spinner {
height: 100px;
width: 100px;
opacity: 0.2;
animation: spin 3s linear infinite;
}
}

@keyframes color-change {
0% {
Expand Down
1 change: 1 addition & 0 deletions app/images/graphics/guarantee.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions app/images/icons/checkmark-neon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const roughUnderlineElements = document.querySelectorAll('.rough-underline')
const roughHighlightElements = document.querySelectorAll('.rough-highlight')
const wavingElement = document.querySelector('.waving-hand')

wavingObserver.observe(wavingElement)
if (wavingElement) wavingObserver.observe(wavingElement)

roughUnderlineElements.forEach((element) => {
roughUnderlineObserver.observe(element)
Expand Down
Loading

0 comments on commit 9293859

Please sign in to comment.