I have java+react app. Want to implement fixed price subscriptions. I did follow this sample github but looks like Im missing something.
After customer entered card I can see payment processed in stripe dashboard but subscription still in 'INCOMPLETE' status. The question is similar to this one but I still have a problem even after ON_SUBSCRIPTION added.
Java part:
@GetMapping("/stripe/prices")
public Response<PricesResponse> prices() {
RequestOptions options = RequestOptions.builder()
.setApiKey(apiKey)
.build();
// search customer
try {
CustomerSearchParams params =
CustomerSearchParams.builder()
.setQuery("name:'aaa bbb'")
.build();
CustomerSearchResult customers = Customer.search(params, options);
List<String> names = customers.getData()
.stream()
.map(Customer::getName)
.toList();
logger.info(String.valueOf(names));
} catch (StripeException e) {
throw new RuntimeException(e);
}
// create customer
Customer customer;
try {
String customerName = "Jenny Rosen " + System.currentTimeMillis();
System.out.println(customerName);
CustomerCreateParams params =
CustomerCreateParams.builder()
.setName(customerName)
.setEmail("jennyrosen@example.com")
.build();
customer = Customer.create(params, options);
} catch (StripeException e) {
throw new RuntimeException(e);
}
// get prices
PriceCollection prices = new PriceCollection();
try {
PriceListParams params = PriceListParams
.builder()
.build();
prices = Price.list(params, options);
} catch (StripeException e) {
throw new RuntimeException(e);
}
// create subscriptions
Subscription subscription;
try {
SubscriptionCreateParams.PaymentSettings paymentSettings =
SubscriptionCreateParams.PaymentSettings
.builder()
.setSaveDefaultPaymentMethod(SubscriptionCreateParams.PaymentSettings.SaveDefaultPaymentMethod.ON_SUBSCRIPTION)
.build();
SubscriptionCreateParams subCreateParams = SubscriptionCreateParams
.builder()
.setCustomer(customer.getId())
.addItem(
SubscriptionCreateParams
.Item.builder()
.setPrice(prices.getData().get(0).getId())
.build()
)
.setPaymentSettings(paymentSettings)
.setPaymentBehavior(SubscriptionCreateParams.PaymentBehavior.DEFAULT_INCOMPLETE)
.addAllExpand(Arrays.asList("latest_invoice.payment_intent"))
.build();
subscription = Subscription.create(subCreateParams, options);
} catch (StripeException e) {
throw new RuntimeException(e);
}
// create payment intent
PaymentIntent paymentIntent;
try {
PaymentIntentCreateParams params =
PaymentIntentCreateParams.builder()
.setCustomer(customer.getId())
.setAmount(prices.getData().get(0).getUnitAmount())
.setCurrency("usd")
.build();
paymentIntent = PaymentIntent.create(params, options);
} catch (StripeException e) {
throw new RuntimeException(e);
}
return new Response<>(new PricesResponse(publishableKey, paymentIntent.getClientSecret(), prices.getData()
.stream()
.map(price -> new PriceResponse(price.getId(), price.getNickname(), price.getUnitAmount()))
.toList()));
}
React part:
Prices page
const Prices = () => {
const navigate = useNavigate();
const [prices, setPrices] = useState([]);
const [clientSecret, setClientSecret] = useState("");
useEffect(() => {
doRestCall('/stripe/prices', 'get', null, null,
(response) => {
setPrices(response.body.prices)
setClientSecret(response.body.clientSecret)
})
}, [])
function toCheckout() {
navigate('/checkout', {
state: {
clientSecret
}
})
}
return (
<div>
<h1>Select a plan</h1>
<div className="price-list">
{prices.map((price) => {
return (
<div key={price.id}>
<h3>{price.name}</h3>
<p>
${price.amount / 100} / month
</p>
<button onClick={() => toCheckout()}>
Select
</button>
</div>
)
})}
</div>
</div>
)}
Checkout page
const Checkout = () => {
const {
state: {
clientSecret,
}
} = useLocation();
const stripe = useStripe();
const elements = useElements();
const [name, setName] = useState('Jenny Rosen');
const [messages, setMessages] = useState('');
const navigate = useNavigate();
const handleSubmit = async (e) => {
e.preventDefault();
const cardElement = elements.getElement(CardElement);
const { error } = await stripe.confirmCardPayment(clientSecret, {
payment_method: {
card: cardElement,
billing_details: {
name: name,
}
}
});
if(error) {
// show error and collect new card details.
setMessages(error.message);
return;
}
navigate('/complete', {
state: {
clientSecret
}
});
};
return (<>
<h1>Subscribe</h1>
<p>
Try the successful test card: <span>4242424242424242</span>.
</p>
<p>
Try the test card that requires SCA: <span>4000002500003155</span>.
</p>
<p>
Use any <i>future</i> expiry date, CVC,5 digit postal code
</p>
<hr/>
<form onSubmit={handleSubmit}>
<label>
Full name
<input type="text" id="name" value={name} onChange={(e) => setName(e.target.value)}/>
</label>
<CardElement/>
<button>
Subscribe
</button>
<div>{messages}</div>
</form>
</>);
Also question about subscription flow in How subscriptions work page. There is 'invoice' mentioned. Should I handle it manually. I do see /invoice-preview on java side in GitHub sample, but can't find where it is called from react part.
A successful payment to the subscription's late_invoice
will move its status to active
. Therefore, instead of creating a new PaymentIntent and return its clientSecret, You should just return the clientSecret of the PaymentIntent associated with the subscription's latest_invoice
.
You can find example code in the integration guide