Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src-ts/config/environments/environment.default.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const EnvironmentConfigDefault: EnvironmentConfigModel = {
SERVICE: 'platform-ui',
},
MEMBER_VERIFY_LOOKER: 3322,
REACT_APP_ENABLE_TCA_CERT_MONETIZATION: process.env.REACT_APP_ENABLE_TCA_CERT_MONETIZATION as unknown as boolean || false,
REAUTH_OFFSET: 55,
SPRIG: {
ENVIRONMENT_ID: 'bUcousVQ0-yF',
Expand Down
3 changes: 2 additions & 1 deletion src-ts/lib/global-config.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,6 @@ export interface GlobalConfig {
UNIVERSAL_NAV: {
URL: string
}
MEMBER_VERIFY_LOOKER: number
MEMBER_VERIFY_LOOKER: number,
REACT_APP_ENABLE_TCA_CERT_MONETIZATION: boolean
}
4 changes: 4 additions & 0 deletions src-ts/lib/styles/_typography.scss
Original file line number Diff line number Diff line change
Expand Up @@ -309,3 +309,7 @@ h4 {
line-height: 20px;
font-weight: $font-weight-medium;
}

.pointer-events-none {
pointer-events: none;
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ import { PerksSection } from '../perks-section'
import { PageLayout } from '../page-layout'
import { EnrolledModal } from '../enrolled-modal'
import { getTCACertificationPath } from '../../learn.routes'
import { EnvironmentConfig } from '../../../../config'
import { StripeProduct, useGetStripeProduct } from '../../learn-lib/data-providers/payments'

import { EnrollmentSidebar } from './enrollment-sidebar'
import { EnrollmentFormValue } from './enrollment-form/enrollment-form.config'

const enrollmentBreadcrumb: Array<BreadcrumbItemModel> = [{ name: 'Enrollment', url: '' }]

Expand All @@ -52,6 +53,10 @@ const EnrollmentPage: FC<{}> = () => {
ready: certificationReady,
}: TCACertificationProviderData = useGetTCACertification(dashedName as string)

// fetch Stripe product data
const { product }: { product: StripeProduct | undefined }
= useGetStripeProduct(certification?.stripeProductId as string)

// Fetch Enrollment status & progress
const {
progress,
Expand All @@ -77,23 +82,18 @@ const EnrollmentPage: FC<{}> = () => {
}
}

const startEnrollFlow: (value?: EnrollmentFormValue) => Promise<void>
= useCallback(async (value?: EnrollmentFormValue): Promise<void> => {
if (!profile) {
return
}

if (value?.email) {
userInfo.current = { ...userInfo.current, email: value.email }
return
}
const startEnrollFlow: () => Promise<void>
= useCallback(async (): Promise<void> => {
if (!profile) {
return
}

await enrollTCACertificationAsync(`${profile.userId}`, `${certification.id}`)
.then(d => {
setIsEnrolledModalOpen(true)
setCertificateProgress(d)
})
}, [certification?.id, profile, setCertificateProgress])
await enrollTCACertificationAsync(`${profile.userId}`, `${certification.id}`)
.then(d => {
setIsEnrolledModalOpen(true)
setCertificateProgress(d)
})
}, [certification?.id, profile, setCertificateProgress])

function navToCertificationDetails(): void {
navigate(getTCACertificationPath(certification.dashedName))
Expand All @@ -110,20 +110,22 @@ const EnrollmentPage: FC<{}> = () => {
<PerksSection
style='clear'
items={perks}
title='Enroll now for Free!'
title={EnvironmentConfig.REACT_APP_ENABLE_TCA_CERT_MONETIZATION
? 'Enroll now with our introductory low pricing!'
: 'Enroll now for Free!'}
/>

<EnrolledModal
isOpen={isEnrolledModalOpen}
onClose={closeEnrolledModal}
/>
</>
) : null
) : undefined
}

function renderSidebar(): ReactNode {
return (
<EnrollmentSidebar profile={profile} onEnroll={startEnrollFlow} />
<EnrollmentSidebar profile={profile} onEnroll={startEnrollFlow} product={product} />
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
@use '../../../../../lib/styles/typography';
@import '../../../../../lib/styles/includes';
@import '../../../../../lib/styles/inputs';

.payment-form {
.label {
@extend .body-ultra-small;
@extend .ultra-small-bold;
margin-bottom: $space-xs;
}

.cardElement {
@include formInputText;

input,
select {
@include formInputText;
}
}

.cardDate {
margin-right: $space-sm;
}

.input-wrap-wrapper {
display: flex;
margin-bottom: $space-sm;
> div {
flex: 1;
}
}

.checkbox-label {
@extend .body-main;
white-space: pre-wrap;
font-weight: 400;
.link {
@extend .body-main-link;
color: $link-blue-dark;
margin-left: $space-xs;
cursor: pointer;
}
}

.pay-button {
width: 100%;
}

.error {
color: $red-100;
margin-bottom: $space-xl;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import { Dispatch, SetStateAction, useState } from 'react'
import classNames from 'classnames'

import {
CardCvcElement,
CardExpiryElement,
CardNumberElement,
} from '@stripe/react-stripe-js'
import {
StripeCardCvcElementChangeEvent,
StripeCardExpiryElementChangeEvent,
StripeCardNumberElementChangeEvent,
} from '@stripe/stripe-js'

import { Button, InputText, LoadingSpinner, OrderContractModal } from '../../../../../lib'
import { InputWrapper } from '../../../../../lib/form/form-groups/form-input/input-wrapper'

import styles from './EnrollPaymentForm.module.scss'

interface PermiumSubFormData {
subsContract: boolean
price: string
}

interface FieldDirtyState {
cardComplete: boolean
expiryComplete: boolean
cvvComplete: boolean
}

interface EnrollPaymentFormProps {
error: boolean
formData: PermiumSubFormData
isFormValid: boolean
onPay: () => void
onUpdateField: (fieldName: string, value: string | boolean) => void
isPayProcessing: boolean
}

type CardChangeEvent
= StripeCardExpiryElementChangeEvent | StripeCardNumberElementChangeEvent | StripeCardCvcElementChangeEvent

const EnrollPaymentForm: React.FC<EnrollPaymentFormProps> = (props: EnrollPaymentFormProps) => {
const [cardNumberError, setCardNumberError]: [string, Dispatch<string>] = useState<string>('')
const [cardExpiryError, setCardExpiryError]: [string, Dispatch<string>] = useState<string>('')
const [cardCVVError, setCardCVVError]: [string, Dispatch<string>] = useState<string>('')

const [formDirtyState, setFormDirtyState]: [FieldDirtyState, Dispatch<SetStateAction<FieldDirtyState>>]
= useState<FieldDirtyState>({
cardComplete: false,
cvvComplete: false,
expiryComplete: false,
})

const [isOrderContractModalOpen, setIsOrderContractModalOpen]: [boolean, Dispatch<boolean>]
= useState<boolean>(false)

const getError: (data: any) => string = data => data?.error?.message || ''

const onOpenOrderContract: (event: React.SyntheticEvent) => void = event => {
event.preventDefault()
event.stopPropagation()
setIsOrderContractModalOpen(true)
}

const renderCheckboxLabel: () => JSX.Element = () => (
<div className={styles['checkbox-label']}>
Yes, I understand and agree to Topcoder Academy’s
<span className={styles.link} onClick={onOpenOrderContract}>Terms & Conditions</span>
</div>
)

function cardElementOnChange(fieldName: string, data: CardChangeEvent, stateUpdater: Dispatch<string>): void {
const error: string = getError(data)
stateUpdater(error)
props.onUpdateField(fieldName, data.complete)
setFormDirtyState({
...formDirtyState,
[fieldName]: true,
})
}

return (
<div className={classNames(styles['payment-form'], props.isPayProcessing ? 'pointer-events-none' : '')}>
<OrderContractModal
isOpen={isOrderContractModalOpen}
onClose={() => setIsOrderContractModalOpen(false)}
/>

<div className={styles.label}>Card Information</div>

<div className={styles['input-wrap-wrapper']}>
<InputWrapper
label='Card Number'
tabIndex={2}
type='text'
disabled={false}
error={cardNumberError}
hideInlineErrors={false}
dirty={formDirtyState.cardComplete}
>
<CardNumberElement
options={{
classes: {
base: styles.cardElement,
},
}}
onChange={(event: StripeCardNumberElementChangeEvent) => cardElementOnChange('cardComplete', event, setCardNumberError)}
/>
</InputWrapper>
</div>

<div className={styles['input-wrap-wrapper']}>
<InputWrapper
className={styles.cardDate}
label='Date'
tabIndex={3}
type='text'
disabled={false}
error={cardExpiryError}
dirty={formDirtyState.expiryComplete}
>
<CardExpiryElement
options={{
classes: {
base: styles.cardElement,
},
placeholder: 'MM/YY',
}}
onChange={(event: StripeCardExpiryElementChangeEvent) => cardElementOnChange('expiryComplete', event, setCardExpiryError)}
/>
</InputWrapper>
<InputWrapper
label='CVC'
tabIndex={3}
type='text'
disabled={false}
error={cardCVVError}
dirty={formDirtyState.cvvComplete}
>
<CardCvcElement
options={{
classes: {
base: styles.cardElement,
},
placeholder: 'CCV',
}}
onChange={(event: StripeCardCvcElementChangeEvent) => cardElementOnChange('cvvComplete', event, setCardCVVError)}
/>
</InputWrapper>
</div>

<InputText
label={renderCheckboxLabel()}
name='order-contract'
tabIndex={1}
type='checkbox'
checked={props.formData.subsContract}
onChange={event => props.onUpdateField('subsContract', event.target.checked)}
/>

{
props.error && (
<div className={styles.error}>
Your card was declined. Please try a different card.
</div>
)
}

{
props.isPayProcessing && (
<LoadingSpinner type='Overlay' />
)
}

<Button
className={styles['pay-button']}
size='lg'
type='button'
buttonStyle='primary'
name='pay-button'
label={`Pay ${props.formData.price} and enroll`}
disable={!props.isFormValid || props.isPayProcessing}
onClick={props.onPay}
/>
</div>
)
}

export default EnrollPaymentForm
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as EnrollPaymentForm } from './EnrollPaymentForm'
Loading