javareactjsstripe-payments

Stripe subscription not updating to ACTIVE after payment


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.


Solution

  • 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