invertase / stripe-firebase-extensions

Repository of Firebase Extensions built by Stripe.
https://firebase.google.com/products/extensions
Apache License 2.0
438 stars 173 forks source link

Subscription payments Mobile #352

Closed unxavi closed 1 year ago

unxavi commented 2 years ago

Feature request

Is your feature request related to a problem? Please describe.

When integrating this extension, the docs for the setup says Subscription payments (web only) why is that? what makes it hard to also integrate subscriptions for mobile devices? Could it be possible to work around this limitation?

On the recommended usage for the extension at the URL https://github.com/stripe/stripe-firebase-extensions/tree/next/firestore-stripe-payments

It stays:

If you're developing native mobile applications and you're selling digital products or services within your app, (e.g. subscriptions, in-game currencies, game levels, access to premium content, or unlocking a full version), you must use the app store's in-app purchase APIs. See [Apple's](https://developer.apple.com/app-store/review/guidelines/#payments) and [Google's](https://support.google.com/googleplay/android-developer/answer/9858738?hl=en&ref_topic=9857752) guidelines for more information.

Which is not totally true

Apple Guidelines for example: https://developer.apple.com/app-store/review/guidelines/#payments

3.1.3(d) Person-to-Person Services: If your app enables the purchase of real-time person-to-person services between two individuals (for example tutoring students, medical consultations, real estate tours, or fitness training), you may use purchase methods other than in-app purchase to collect those payments.

3.1.3(e) Goods and Services Outside of the App: If your app enables people to purchase physical goods or services that will be consumed outside of the app, you must use purchase methods other than in-app purchase to collect those payments, such as Apple Pay or traditional credit card entry.

My current use case is a subscription Person-to-Person that will be consumed outside of the app. So I'm affected by both rules and I must use a different purchase methods other than in-app purchase to be compliant.

So I don't understand why the extension limit this use cases.

Describe the solution you'd like

I would like to use the extension to subscribe the users of a mobile app to stripe, since we are selling services outside of the app.

jesus-mg-ios commented 2 years ago

Hi: @dackers86, Do you know when is it going to be develop? . I think that it's a very interesting feature

dackers86 commented 2 years ago

HI @jesus-mg-ios @unxavi.

I added the enhancement tag as I initially thought could require some development. However, could you provide a use case for this scenario?

The extension itself is simply use the Stripe Api at it's core. If this technique can be used through Stripe, I cannot a reason as to why we would limited through the extension?

Could this merely be a documentation update, or has anyone encountered a scenario where this type of request has not been possible?

unxavi commented 2 years ago

@dackers86 thanks for getting back to me.

Use case

Delivery Service

I want a user to be able to subscribe to a delivery service, the user pays a subscription fee and can order anything they want to be delivery, the service is consumed outside of the app, so on the AppStore guidelines you are asked to implement this with a payment system different than In App Purchases, if you do with IAP you are in violation of the guidelines, so Stripe mobile subscription is perfect for this use case.

3.1.3(e) Goods and Services Outside of the App: If your app enables people to purchase physical goods or services that will be consumed outside of the app, you must use purchase methods other than in-app purchase to collect those payments, such as Apple Pay or traditional credit card entry.

Teaching

Another user case, you are music teacher which you have students that subscribe to your music classes through the application on the store, the classes are done through a video conference on a one to one class. One teacher - One student.

By the AppStore guidelines you could use other method different from In App Purchases, getting around the 30% fee of the AppStore even if you make the video conference inside the app.

3.1.3(d) Person-to-Person Services: If your app enables the purchase of real-time person-to-person services between two individuals (for example tutoring students, medical consultations, real estate tours, or fitness training), you may use purchase methods other than in-app purchase to collect those payments.

In this use case if you decide not to deliver the lesson inside the app, but you use something like a link to zoom, then you are affected by the guideline 3.1.3(e) Goods and Services Outside of the App and you should use another payment different of IAP to comply with the store rules.

The extension API for subscriptions.

As you say, this extension uses the Stripe API and this extension docs says the following:

Subscription payments (web only)

The issue is that for web payments with the extension, you create a document on Firestore and the extension will give you back a URLwhere then you redirect a user to pay on the browser. This is good for a desktop browser experience and this API you can give to the extension the price id for the Stripe subscription.

On the other side, this extension for mobile, lets you make only one time payment, you write to Firestore the document with the following

imagen

And then the extension give you back the paymentIntentClientSecret and ephemeralKeySecret which then you can use inside the mobile app with the SwiftUI PaymentSheet https://stripe.com/docs/payments/accept-a-payment?platform=ios&uikit-swiftui=swiftui

The mode subscription can't be use with the mobile in the extension, this will raise an error

https://github.com/stripe/stripe-firebase-extensions/blob/54a116d156232da10ebd6ebf8e96b4665a8fdeb1/firestore-stripe-payments/functions/src/index.ts#L286

Wrapping up

This extension to work with subscription on a mobile app is incomplete and non functional. We need to be able to use the price id param like for the web, but instead of getting back an URL, we need to get back the ephemeralKeySecret to be able to use it with the SwiftUI object PaymentSheet from stripe.

To be able to accomplish this, we should implement the correct Stripe API to handle this use case, which is possible on this extension.

Example Stripe API Call.

 const subscription = await stripe.subscriptions.create({
      customer: customerId,
      items: [{ plan: priceId],
      payment_behavior: "default_incomplete",
      expand: ["latest_invoice.payment_intent"],
      payment_settings: {
        save_default_payment_method: "on_subscription",
      },
      metadata: {
        firebaseUserUID: userUid,
      },
    });

@dackers86 I think this post is already long enough, if you have any doubts or comments, could you get back? At least to clear any doubt.

Do you think we could implement this? Stripe is great for some use cases on mobile where we can't use IAP or have the option of not using it, and yet this extension does not cover this uses cases.

unxavi commented 2 years ago

@dackers86 any doubt?

dackers86 commented 2 years ago

Hi @unxavi Thanks for the awesome feedback.

We will look at producing an investigative PR based on the above!

var2611 commented 2 years ago

Waiting for update.

SohelIslamImran commented 1 year ago

Waiting. @dackers86 Please merge your PR

SohelIslamImran commented 1 year ago

Here is an example of subscription in mobile implementation

app.post('/payment-sheet-subscription', async (_, res) => {
  const { secret_key } = getKeys();

  const stripe = new Stripe(secret_key as string, {
    apiVersion: '2020-08-27',
    typescript: true,
  });

  const customers = await stripe.customers.list();

  // Here, we're getting latest customer only for example purposes.
  const customer = customers.data[0];

  if (!customer) {
    return res.send({
      error: 'You have no customer created',
    });
  }

  const ephemeralKey = await stripe.ephemeralKeys.create(
    { customer: customer.id },
    { apiVersion: '2020-08-27' }
  );
  const subscription = await stripe.subscriptions.create({
    customer: customer.id,
    items: [{ price: 'price_1L3hcFLu5o3P18Zp9GDQEnqe' }],
    trial_period_days: 3,
  });

  if (typeof subscription.pending_setup_intent === 'string') {
    const setupIntent = await stripe.setupIntents.retrieve(
      subscription.pending_setup_intent
    );

    return res.json({
      setupIntent: setupIntent.client_secret,
      ephemeralKey: ephemeralKey.secret,
      customer: customer.id,
    });
  } else {
    throw new Error(
      'Expected response type string, but received: ' +
        typeof subscription.pending_setup_intent
    );
  }
});
algodextrous commented 1 year ago
router.post("/create-customer-and-subscription", async (req, res) => {
  try {
    let customer;
    let paymentMethod = req.body.paymentMethodId;
    let stripeEmail = req.body.stripeEmail;
    let customerId = req.body.customerId;
    if (!paymentMethod) {
      paymentMethod = "pm_card_visa";
    }
    try {
      customer = await stripe.customers.retrieve(customerId);

      // Retrieve new payment method
      let newPaymentMethod = await stripe.paymentMethods.retrieve(paymentMethod);
      // Retrieve current default payment method
      let currentPaymentMethod = null;
      if (customer.invoice_settings.default_payment_method) {
        currentPaymentMethod = await stripe.paymentMethods.retrieve(customer.invoice_settings.default_payment_method);
      }
      // Compare new payment method and current payment method
      if (!currentPaymentMethod || (newPaymentMethod.card.last4 !== currentPaymentMethod.card.last4 ||
        newPaymentMethod.card.exp_month !== currentPaymentMethod.card.exp_month ||
        newPaymentMethod.card.exp_year !== currentPaymentMethod.card.exp_year)) {
        // Attach payment method to the customer
        await stripe.paymentMethods.attach(
          paymentMethod,
          { customer: customer.id }
        );
        // Update customer's default payment method
        await stripe.customers.update(customer.id, {
          invoice_settings: {
            default_payment_method: paymentMethod
          },
        });
      } else {
        paymentMethod = currentPaymentMethod.id;
      }
    } catch (err) {
      console.log("The customer doesn't exist. Creating one...");
      customer = await stripe.customers.create({
        email: stripeEmail,
        name: "Customer Name",
        address: {
          line1: "Address Line 1",
          postal_code: "110092",
          city: "New Delhi",
          state: "Delhi",
          country: "India",
        },
        payment_method: paymentMethod,
        invoice_settings: {
          default_payment_method: paymentMethod
        },
      });
    }

    const ephemeralKey = await stripe.ephemeralKeys.create(
      { customer: customer.id },
      { apiVersion: "2022-11-15" }
    );

    const setupIntent = await stripe.setupIntents.create({
      customer: customer.id,
      payment_method_types: ['card'],
      usage: 'off_session',
    });

    const subscription = await stripe.subscriptions.create({
      customer: customer.id,
      items: [
        {
          price: 'price_1NGcpvSDKjg4kQFbTszkhOy4',
        },
      ],
      default_payment_method: paymentMethod,
      payment_behavior: 'allow_incomplete',
      payment_settings: { save_default_payment_method: 'on_subscription' },
      expand: ['latest_invoice.payment_intent'],
    });

    res.status(200).json({
      customer: customer.id,
      publishableKey: Publishable_Key,
      setupIntent: setupIntent.client_secret,
      ephemeralKey: ephemeralKey.secret,
      subscription: subscription.id,
      paymentIntent: subscription.latest_invoice.payment_intent.client_secret,
    });
  } catch (err) {
    console.error(`Error: ${err}`);
    res.status(err.statusCode).send(err.raw.message);
  }
});
rahulsain commented 1 year ago

PR merged, now waiting for official integration docs

rahulsain commented 1 year ago

is official docs now updated, for how to use mobile subscription?

dackers86 commented 1 year ago

@rahulsain Thanks, you're correct, documentation needs to be added.

I'll close this issue as the feature has been added. Please track this new issue for documentation updates.