Skip to content

Commit

Permalink
Add a begging modal (#7079)
Browse files Browse the repository at this point in the history
* Start work on begging modal

* Add modal, adjust things, add props (#7080)

* Add modal, adjust things, add props

* Add success modal

* Progress further

* Add success + cancel flow

* Change success button close flow

* Fix path and improve tests

---------

Co-authored-by: Jeremy Walker <jez.walker@gmail.com>

---------

Co-authored-by: Aron Demeter <66035744+dem4ron@users.noreply.github.com>
  • Loading branch information
iHiD and dem4ron authored Sep 30, 2024
1 parent f9bfb9c commit 45945bd
Show file tree
Hide file tree
Showing 12 changed files with 387 additions and 17 deletions.
4 changes: 4 additions & 0 deletions app/css/ui-kit/colors.css
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,8 @@
--textColorMentorTrackSelectorSelected: theme(colors.smokeGray);
--textColorProgressTextCompleted: theme(colors.darkSuccessGreen);
--c-prominent-link-color: theme(colors.lightBlue);
--textGradientStart: #2200ff;
--textGradientEnd: #9e00ff;

--shadowColorED: 0, 80, 255;
--shadowColorMain: 79, 114, 205;
Expand Down Expand Up @@ -540,6 +542,8 @@ body.namespace-.controller-insiders.theme-light,
--textColorMentoringRequestedStatusInfo: theme(colors.aliceBlue);
--textColorMentorTrackSelectorSelected: theme(colors.aliceBlue);
--textColorProgressTextCompleted: theme(colors.greenPrompt);
--textGradientStart: #8370ff;
--textGradientEnd: #b947ff;

--c-prominent-link-color: theme(colors.lightGold);
--color22: var(--c-533F56);
Expand Down
5 changes: 4 additions & 1 deletion app/css/ui-kit/text.css
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,10 @@

.text-gradient {
color: #6a01ff;
background: -webkit-linear-gradient(#2200ff, #9e00ff);
background: -webkit-linear-gradient(
var(--textGradientStart),
var(--textGradientEnd)
);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
Expand Down
49 changes: 49 additions & 0 deletions app/helpers/react_components/modals/beg_modal.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
module ReactComponents
module Modals
class BegModal < ReactComponent
def to_s
return unless show_modal?

super("beg-modal", {
previous_donor:,
request: {
endpoint: Exercism::Routes.current_api_payments_subscriptions_url,
options: {
initial_data: AssembleCurrentSubscription.(current_user)
}
},
links: {
settings: Exercism::Routes.donations_settings_url,
success: Exercism::Routes.dashboard_url,
hide_introducer: Exercism::Routes.hide_api_settings_introducer_path(slug)
}
})
end

memoize
def show_modal?
return false if current_user.current_subscription
return false if current_user.donated_in_last_35_days?
return false unless current_user.solutions.count >= 5

dismissal = current_user.dismissed_introducers.find_by(slug:)
return true unless dismissal

if dismissal.created_at < 1.month.ago
dismissal.destroy
return true
end

false
end

memoize
def previous_donor = current_user.total_donated_in_dollars.positive?

private
def slug
"beg-modal"
end
end
end
end
5 changes: 4 additions & 1 deletion app/javascript/components/donations/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ import { StripeForm } from './StripeForm'
import currency from 'currency.js'
import { Request, useRequestQuery } from '../../hooks/request-query'
import { FetchingBoundary } from '../FetchingBoundary'
import { FormWithModalLinks } from './FormWithModal'

export type StripeFormLinks = {
success: string
settings: string
}
const TabsContext = createContext<TabContext>({
current: 'subscription',
switchToTab: () => null,
Expand Down
20 changes: 14 additions & 6 deletions app/javascript/components/donations/SuccessModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ import currency from 'currency.js'

const badge = { rarity: 'rare' as BadgeRarity, iconName: 'supporter' }

export default ({
export default function ({
amount,
open,
closeLink,
handleCloseModal,
}: {
amount: currency | null
open: boolean
closeLink: string
}): JSX.Element => {
closeLink?: string
handleCloseModal?: () => void
}): JSX.Element {
return (
<Modal open={open} onClose={() => null} className="m-donation-confirmation">
<GraphicalIcon icon="completed-check-circle" className="main-icon" />
Expand All @@ -36,9 +38,15 @@ export default ({
</div>
</div>

<a href={closeLink} className="btn-primary btn-l w-100">
Happy to help! I&apos;m done here 👍
</a>
{closeLink ? (
<a href={closeLink} className="btn-primary btn-l w-100">
Happy to help! I&apos;m done here 👍
</a>
) : (
<button onClick={handleCloseModal} className="btn-primary btn-l w-100">
Happy to help! I&apos;m done here 👍
</button>
)}
</Modal>
)
}
207 changes: 207 additions & 0 deletions app/javascript/components/modals/BegModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import React, { lazy, Suspense, useCallback, useState } from 'react'
import currency from 'currency.js'
import { PaymentIntentType } from '@/components/donations/stripe-form/useStripeForm'
import { Request } from '@/hooks/request-query'
import { StripeFormLinks } from '@/components/donations/Form'
import { Modal } from '.'
import SuccessModal from '../donations/SuccessModal'
import { useMutation } from '@tanstack/react-query'
import { sendRequest } from '@/utils/send-request'
import { ErrorBoundary, ErrorMessage } from '@/components/ErrorBoundary'
const Form = lazy(() => import('@/components/donations/Form'))

export default function ({
previousDonor,
links,
request,
}: {
previousDonor: boolean
links: StripeFormLinks & { hideIntroducer: string }
request: Request
}): JSX.Element {
const [isModalOpen, setIsModalOpen] = useState(true)
const [isSuccessModalOpen, setIsSuccessModalOpen] = useState(false)
const [paymentAmount, setPaymentAmount] = useState<currency | null>(null)

const handleSuccess = useCallback(
(_type: PaymentIntentType, amount: currency) => {
setIsModalOpen(false)
setPaymentAmount(amount)
setIsSuccessModalOpen(true)
},
[]
)

const {
mutate: hideIntroducer,
status,
error,
} = useMutation(
() => {
const { fetch } = sendRequest({
endpoint: links.hideIntroducer,
method: 'PATCH',
body: null,
})

return fetch
},
{
onSuccess: () => {
setIsModalOpen(false)
},
}
)

const handleContinueWithoutDonating = useCallback(() => {
hideIntroducer()
}, [])

const DEFAULT_ERROR = new Error('Unable to dismiss modal')

return (
<>
<Modal
style={{ content: { maxWidth: 'fit-content' } }}
cover
aria={{ modal: true, describedby: 'a11y-finish-mentor-discussion' }}
className="m-finish-student-mentor-discussion"
containerClassName="!p-48"
ReactModalClassName="bg-unnamed15"
shouldCloseOnOverlayClick={false}
shouldCloseOnEsc={false}
open={isModalOpen}
onClose={() => setIsModalOpen(false)}
>
<div
id="a11y-finish-mentor-discussion"
className="flex lg:flex-row flex-col"
>
<div className="lg:mr-64 mr-0 lg:max-w-[700px] max-w-full">
<h3 className="text-h4 mb-4 text-prominentLinkColor">
Sorry to disturb, but...
</h3>
<h1 className="text-h1 mb-12">We need your help!</h1>

<div className="mb-20 pb-20 border-b-1 border-borderColor7">
{previousDonor ? <PreviousDonorContent /> : <NonDonorContent />}
</div>

<h3 className="text-h4 mb-6">Can't afford it?</h3>
<p className="text-p-large mb-20">
If you can&apos;t afford to donate, but would like to help in some
other way, please share Exercism with your friends and colleagues,
and shout about us on social media. The more people that use us,
the more donations we get!
</p>

<h3 className="text-h4 mb-6">Want to know more?</h3>
<p className="text-p-large mb-20">
I put together a short video that explains why we need donations
and how we use them 👇
</p>

<div className="c-youtube-container mb-32">
<iframe
width="560"
height="315"
frameBorder="0"
src="https://player.vimeo.com/video/855534271?h=97d3a4c8c2&amp;badge=0&amp;autopause=0&amp;player_id=0&amp;app_id=58479&transparent=0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
<script src="https://player.vimeo.com/api/player.js" />
</div>
</div>
<div className="flex flex-col items-end bg-transparent">
<div className="lg:w-[564px] w-100 shadow-lgZ1 rounded-8 mb-20">
<Suspense fallback={<div className="c-loading-suspense" />}>
<Form
request={request}
defaultAmount={{
payment: currency(16),
subscription: currency(16),
}}
userSignedIn={true}
captchaRequired={false}
links={links}
onSuccess={handleSuccess}
/>
</Suspense>
</div>

<div className="lg:w-[564px] w-100 border-2 border-gradient bg-lightPurple py-12 px-24 rounded-8">
<p className="text-gradient font-semibold leading-160 text-17">
Fewer than 1% of who use Exercism choose to donate. If you can
afford to do so, please be one of them.
</p>
</div>
</div>
</div>
<div className="flex items-center lg:border-t-1 border-borderColor6 pt-20">
<button
onClick={handleContinueWithoutDonating}
className="btn-enhanced btn-l !shadow-xsZ1v3 py-16 px-24"
>
Continue without donating
</button>
<ErrorBoundary resetKeys={[status]}>
<ErrorMessage error={error} defaultError={DEFAULT_ERROR} />
</ErrorBoundary>
</div>
</Modal>
<SuccessModal
open={isSuccessModalOpen}
amount={paymentAmount}
handleCloseModal={() => setIsSuccessModalOpen(false)}
/>
</>
)
}

function PreviousDonorContent() {
return (
<>
<p className="text-p-large mb-12">
You're one of the few people who have donated to Exercism. Thank you so
much for supporting us 💙
</p>
<p className="text-p-large mb-12">
I hate to ask you again (😔), but we really need your support. Exercism
isn't covering its costs and we really need your help. If you're
enjoying Exercism and can afford it,{' '}
<strong className="font-medium">
please consider donating a few more dollars{' '}
</strong>
to support us.
</p>

<p className="text-p-large">
If possible, a monthly donation would be extra helpful! It takes 30
seconds to setup using the form on the right 👉
</p>
</>
)
}

function NonDonorContent() {
return (
<>
<p className="text-p-large mb-12">
Exercism relies on donations. But right now we don't have enough 😔
</p>
<p className="text-p-large mb-12">
Most people who use Exercism can't afford to donate. But if you can, and
you're finding Exercism useful,{' '}
<strong className="font-medium">
please consider donating a few dollars{' '}
</strong>
so that we can keep it free for those who can't afford it.
</p>
<p className="text-p-large">
If only 1% of our users support us regularly, we'll be able to cover our
costs. It takes 30 seconds to donate, using the form on the right 👉
</p>
</>
)
}
4 changes: 3 additions & 1 deletion app/javascript/components/modals/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type ModalProps = Omit<Props, 'isOpen' | 'onRequestClose'> & {
theme?: Theme
aria?: Aria
ReactModalClassName?: string
containerClassName?: string
}

export function Modal({
Expand All @@ -28,6 +29,7 @@ export function Modal({
children,
aria,
ReactModalClassName,
containerClassName,
...props
}: React.PropsWithChildren<ModalProps>): JSX.Element {
const overlayClassNames = [
Expand Down Expand Up @@ -70,7 +72,7 @@ export function Modal({
</ActiveBackground>
)}
>
<div className="--modal-container">
<div className={`--modal-container ${containerClassName}`}>
{closeButton ? <CloseButton onClose={onClose} /> : null}
{contentElement}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function DonationStep({
</h1>
<p className="text-p-large mb-12">
Exercism relies on donations from wonderful people like you to keep us
financially afloat. Currently, not enough people are donating to
financially alive. Currently, not enough people are donating to
Exercism and we may have to shut down the site. With your help, we can
keep the lights on, and also grow and expand our work.{' '}
<strong className="font-medium">
Expand Down Expand Up @@ -94,3 +94,5 @@ export function DonationStep({
</div>
)
}

export default DonationStep
Loading

0 comments on commit 45945bd

Please sign in to comment.