Skip to content

[Do Not Merge] poc with stripe credit card elements #2367

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: develop
Choose a base branch
from
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import React, {useState} from 'react'
import PropTypes from 'prop-types'
import {defineMessage, FormattedMessage, useIntl} from 'react-intl'
import {loadStripe} from '@stripe/stripe-js'
import {Elements} from '@stripe/react-stripe-js';
import {
Box,
Button,
Expand All @@ -32,12 +34,14 @@ import {
ToggleCardEdit,
ToggleCardSummary
} from '@salesforce/retail-react-app/app/components/toggle-card'
import PaymentForm from '@salesforce/retail-react-app/app/pages/checkout/partials/payment-form'
import PaymentForm from '@salesforce/retail-react-app/app/pages/checkout/partials/sf-payment-form'
import ShippingAddressSelection from '@salesforce/retail-react-app/app/pages/checkout/partials/shipping-address-selection'
import AddressDisplay from '@salesforce/retail-react-app/app/components/address-display'
import {PromoCode, usePromoCode} from '@salesforce/retail-react-app/app/components/promo-code'
import {API_ERROR_MESSAGE} from '@salesforce/retail-react-app/app/constants'

const stripePromise = loadStripe('pk_test_JsbBx7imKb7n7Xtlb5MH5BNO00ttiURmPV');

const Payment = () => {
const {formatMessage} = useIntl()
const {data: basket} = useCurrentBasket()
Expand Down Expand Up @@ -97,6 +101,30 @@ const Payment = () => {
body: paymentInstrument
})
}

const handlePaymentMethod = (paymentMethod) => {
console.log('Got payment method in parent:', paymentMethod);

const paymentInstrument = {
paymentMethodId: 'CREDIT_CARD',
paymentCard: {
holder: paymentMethod.billing_details.name,
maskedNumber: '************' + paymentMethod.card.last4,
cardType:
paymentMethod.card.brand.charAt(0).toUpperCase() +
paymentMethod.card.brand.slice(1),
expirationMonth: paymentMethod.card.exp_month,
expirationYear: paymentMethod.card.exp_year
}
}

return addPaymentInstrumentToBasket({
parameters: {basketId: basket?.basketId},
body: paymentInstrument
})
// You can now send paymentMethod.id to your backend
}

const onBillingSubmit = async () => {
const isFormValid = await billingAddressForm.trigger()

Expand Down Expand Up @@ -169,7 +197,9 @@ const Payment = () => {

<Stack spacing={6}>
{!appliedPayment?.paymentCard ? (
<PaymentForm form={paymentMethodForm} onSubmit={onPaymentSubmit} />
<Elements stripe={stripePromise}>
<PaymentForm form={paymentMethodForm} onPaymentMethod={handlePaymentMethod} />
</Elements>
) : (
<Stack spacing={3}>
<Heading as="h3" fontSize="md">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/*
* Copyright (c) 2025, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import React, {useState} from 'react'
import {
CardElement,
useStripe,
useElements,
CardNumberElement,
CardCvcElement,
CardExpiryElement
} from '@stripe/react-stripe-js'
import axios from 'axios'
import PropTypes from 'prop-types'

const elementStyle = {
base: {
fontSize: '16px',
color: '#000',
'::placeholder': {
color: '#a0a0a0'
},
},
invalid: {
color: '#fa755a',
},
};

const PaymentForm = ({onPaymentMethod}) => {
const stripe = useStripe()
const elements = useElements()
const [processing, setProcessing] = useState(false)
const [cardholderName, setCardholderName] = useState('')
const [errorMessage, setErrorMessage] = useState('')
const [isLoading, setIsLoading] = useState(false)

/*
const handleSubmit = async (event) => {
event.preventDefault()
setProcessing(true)

try {
// Create payment intent on backend
const {data} = await axios.post('/api/payments/create-payment-intent', {
amount: 1000, // Amount in cents
currency: 'usd'
})

const result = await stripe.confirmCardPayment(data.clientSecret, {
payment_method: {
card: elements.getElement(CardElement),
billing_details: {
name: 'Customer Name'
}
}
})

if (result.error) {
console.error(result.error.message)
} else {
// Payment successful
console.log('Payment succeeded')
}
} catch (error) {
console.error('Payment error', error)
}

setProcessing(false)
}
*/
const handleSubmit = async (e) => {
e.preventDefault()
setIsLoading(true)
setErrorMessage('')

if (!stripe || !elements) {
return
}

const cardElement = elements.getElement(CardNumberElement)

const {error, paymentMethod} = await stripe.createPaymentMethod({
type: 'card',
card: cardElement,
billing_details: {
name: cardholderName
}
})

setIsLoading(false)

if (error) {
setErrorMessage(error.message)
} else {
console.log('PaymentMethod:', paymentMethod)
onPaymentMethod(paymentMethod);
//alert(`Success! Card ending in ${paymentMethod.card.last4}`)
}
}
return (
<form onSubmit={handleSubmit} style={{ maxWidth: 400 }}>
<div style={{display: 'flex', flexDirection: 'column', gap: '16px'}}>
<div style={{border: '1px solid #ccc', borderRadius: '4px', padding: '10px'}}>
<input
type="text"
placeholder="Cardholder Name"
value={cardholderName}
onChange={(e) => setCardholderName(e.target.value)}
style={{
border: 'none',
outline: 'none',
fontSize: '16px',
width: '100%',
background: 'transparent',
}}
required
/>
</div>
<div style={{border: '1px solid #ccc', borderRadius: '4px', padding: '10px'}}>
<CardNumberElement options={{style: elementStyle}}/>
</div>
<div style={{border: '1px solid #ccc', borderRadius: '4px', padding: '10px'}}>
<CardExpiryElement options={{style: elementStyle}}/>
</div>
<div style={{border: '1px solid #ccc', borderRadius: '4px', padding: '10px'}}>
<CardCvcElement options={{style: elementStyle}}/>
</div>

{errorMessage && (
<div style={{color: 'red', fontSize: '14px'}}>
{errorMessage}
</div>
)}

<button
type="submit"
disabled={!stripe || isLoading}
style={{
backgroundColor: '#007bff',
color: '#fff',
padding: '12px 24px',
border: 'none',
borderRadius: '4px',
fontSize: '16px',
cursor: 'pointer',
opacity: isLoading ? 0.6 : 1
}}
>
{isLoading ? 'Processing…' : 'Pay'}
</button>
</div>

</form>
)
}
PaymentForm.propTypes = {
/** Callback for form submit */
onPaymentMethod: PropTypes.func
}

export default PaymentForm
8 changes: 5 additions & 3 deletions packages/template-retail-react-app/app/ssr.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const options = {
mobify: config,

// The port that the local dev server listens on
port: 3000,
port: 3001,

// The protocol on which the development Express app listens.
// Note that http://localhost is treated as a secure context for development,
Expand Down Expand Up @@ -309,14 +309,16 @@ const {handler} = runtime.createHandler(options, (app) => {
],
'script-src': [
// Used by the service worker in /worker/main.js
'storage.googleapis.com'
'storage.googleapis.com',
'https://js.stripe.com'
],
'connect-src': [
// Connect to Einstein APIs
'api.cquotient.com',
// Connect to DataCloud APIs
'*.c360a.salesforce.com'
]
],
'frame-src': ['self', 'https://js.stripe.com']
}
}
})
Expand Down
29 changes: 27 additions & 2 deletions packages/template-retail-react-app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/template-retail-react-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@
"react-helmet": "^6.1.0",
"react-hook-form": "^7.43.9",
"react-intl": "^5.25.1",
"react-router-dom": "^5.3.4"
"react-router-dom": "^5.3.4",
"@stripe/react-stripe-js": "^3.5.1"
},
"devDependencies": {
"cross-env": "^5.2.1"
Expand Down
Loading