--- title: Accept in-person payments route: /terminal/quickstart --- # Accept in-person payments # Accept in-person payments Set up Stripe Terminal and use the simulated reader to emulate accepting in-person payments. ### Install the Stripe Node library Install the library: ``` npm install --save stripe ``` Initialize the Stripe library with your key: ### Install the Stripe Ruby library Install the gem: ``` gem install stripe ``` Initialize the Stripe library with your key: ### Install the Stripe Java library Add the following dependency to your POM and replace {VERSION} with the version number you want to use. ``` com.stripe stripe-java {VERSION} ``` Initialize the Stripe library with your key: ### Install the Stripe Python package Install the package via pip: ``` pip3 install stripe ``` Initialize the Stripe library with your key: ### Install the Stripe PHP library Install the library: ``` composer require stripe/stripe-php ``` Initialize the Stripe library with your key: ### Set up your server Make sure to initialize with Go Modules: ``` go get -u github.com/stripe/stripe-go/v82 ``` Initialize the Stripe library with your key: ### Install the Stripe.net library Install the library: ``` dotnet add package Stripe.net ``` Initialize the Stripe library with your key: ### Install the Stripe libraries Install the libraries: ``` npm install --save stripe @stripe/stripe-js next ``` Initialize the Stripe library with your key: ### Create a location Create locations to organize your readers. Locations allow you to group your readers and automatically download the region-specific configuration for where you’ll use the readers. ### Register a reader A simulated reader lets you get started quickly and build a working integration without requiring physical hardware. Create a simulated reader by using the special registration code `simulated-wpe`. Keep track of your reader ID so you can use it later to process a payment on the reader. ### Create a PaymentIntent Create a PaymentIntent on your server. A PaymentIntent tracks the customer’s payment lifecycle, keeping track of any failed attempts and ensuring they’re only charged once. Store the PaymentIntent ID to be processed later. ### Process the PaymentIntent on your reader When the customer is ready to pay, initiate the payment processing on the reader by passing it the PaymentIntent ID. ### Simulate card presentment on your reader In a real transaction flow, the customer inserts or taps their card on the physical reader. With a simulated reader, you simulate the card presentment step by making another API call. This call successfully confirms the PaymentIntent with a test card. You can also try other test cards. ### Capture the PaymentIntent After the PaymentIntent is confirmed successfully, you can capture it to move the funds. ### Handle errors Each error scenario has a specific code that your application needs to handle. Errors usually require intervention from a cashier in the store, show an appropriate message in your point-of-sale application so they can react accordingly. Refer to the [error handling documentation](https://docs.stripe.com/terminal/payments/collect-card-payment.md?terminal-sdk-platform=server-driven#handle-errors) for more details. ### Run the application Run your server and go to http://localhost:4242. ### Use a test card number to try your integration The simulated reader allows you to test different flows with your point of sale application. For example, you can test different card brands or error scenarios like a declined charge. To enable this behavior, pass a test payment method to the [present payment method](https://docs.stripe.com/api/terminal/readers/present_payment_method.md) API call: | Scenario | Card Number | | ------------------------------- | ---------------- | | Payment succeeds | 4242424242424242 | | Payment requires authentication | 4000002500003155 | | Payment is declined | 4000000000009995 | ### Install the Stripe Node library Install the library: ``` npm install --save stripe ``` Initialize the Stripe library with your key: ### Install the Stripe Ruby library Install the gem: ``` gem install stripe ``` Initialize the Stripe library with your key: ### Install the Stripe Java library Add the following dependency to your POM and replace {VERSION} with the version number you want to use. ``` com.stripe stripe-java {VERSION} ``` Initialize the Stripe library with your key: ### Install the Stripe Python package Install the package via pip: ``` pip3 install stripe ``` Initialize the Stripe library with your key: ### Install the Stripe PHP library Install the library: ``` composer require stripe/stripe-php ``` Initialize the Stripe library with your key: ### Set up your server Make sure to initialize with Go Modules: ``` go get -u github.com/stripe/stripe-go/v82 ``` Initialize the Stripe library with your key: ### Install the Stripe.net library Install the library: ``` dotnet add package Stripe.net ``` Initialize the Stripe library with your key: ### Install the Stripe libraries Install the libraries: ``` npm install --save stripe @stripe/stripe-js next ``` Initialize the Stripe library with your key: ### Create a ConnectionToken endpoint To connect to a reader, your backend needs to give the SDK permission to use the reader with your Stripe account by providing it with the secret from a [ConnectionToken](https://docs.stripe.com/api/terminal/connection_tokens.md). Your backend should only create connection tokens for clients that it trusts. ​​If you’re using Stripe Connect, you should also [scope the connection token](https://docs.stripe.com/terminal/features/connect.md) to the relevant connected accounts. ​​If using locations, you should [pass a location ID](https://docs.stripe.com/terminal/fleet/locations-and-zones.md#connection-tokens) when creating the connection token to control access to readers. ### Organize your readers [Create locations](https://docs.stripe.com/terminal/fleet/locations-and-zones.md) to organize your readers. Locations group readers and allows them to automatically download the reader configuration needed for their region of use. ### Install the SDK This script must always load directly from https://js.stripe.com for compatibility with the latest reader software. Don’t include the script in a bundle or host a copy yourself as this could break your integration without warning. We also provide an npm package that makes it easier to load and use the Terminal JS SDK as a module. For more information, check out [the project on GitHub](https://github.com/stripe/terminal-js). ### Fetch ConnectionToken To give the SDK access to this endpoint, create a function in your web application that requests a ConnectionToken from your backend and returns the secret from the ConnectionToken object. ### Initialize the SDK To initialize a `StripeTerminal` instance in your JavaScript application, provide the `onFetchConnectionToken` function. You must also provide the `onUnexpectedReaderDisconnect` function to handle unexpected disconnects from the reader. ### Discover readers The Stripe Terminal SDK comes with a built-in simulated card reader, so you can develop and test your app without connecting to physical hardware. To use the simulated reader, call `discoverReaders` to search for readers, with the simulated option set to true. ​​If using locations, you can discover intended readers more easily by [filtering by location](https://docs.stripe.com/terminal/fleet/register-readers.md). ### Connect to the simulated reader When `discoverReaders` returns a result, call `connectReader` to connect to the simulated reader. ### Create a PaymentIntent Add an endpoint on your server that creates a PaymentIntent. A PaymentIntent tracks the customer’s payment lifecycle, keeping track of any failed payment attempts and ensuring they’re only charged once. Return the PaymentIntent’s client secret in the response. If you’re using Stripe Connect, you can also specify [connected account information](https://docs.stripe.com/terminal/features/connect.md) based on your platform’s charge logic. ### Fetch the PaymentIntent Make a request to your server for a PaymentIntent to initiate the payment process. ### Collect payment method details Call `collectPaymentMethod` with the PaymentIntent’s client secret to collect a payment method. When connected to the simulated reader calling this method immediately updates the PaymentIntent object with a [simulated test card](https://docs.stripe.com/terminal/references/testing.md#simulated-test-cards). When connected to a physical reader the connected reader waits for a card to be presented. ### Process the payment After successfully collecting payment method data, call `processPayment` with the updated PaymentIntent to process the payment. A successful call results in a PaymentIntent with a status of `requires_capture` for manual capture or `succeeded` for automatic capture. ### Create an endpoint to capture the PaymentIntent Create an endpoint on your backend that accepts a PaymentIntent ID and sends a request to the Stripe API to capture it. ### Capture the PaymentIntent If you defined `capture_method` as `manual` during PaymentIntent creation, the SDK returns an authorized but not captured PaymentIntent to your application. When the PaymentIntent status is `requires_capture`, notify your backend to capture the PaymentIntent. In your request send the PaymentIntent ID. ### Run the application Run your server and go to [localhost:4242](http://localhost:4242). ### Use a test card number to try your integration The simulated reader supports a small amount of configuration, enabling you to test different flows within your point of sale application such as different card brands or error scenarios like a declined charge. To enable this behavior, insert this line of code before you call `collectPaymentMethod.` | Scenario | Card Number | | ------------------------------- | ---------------- | | Payment succeeds | 4242424242424242 | | Payment requires authentication | 4000002500003155 | | Payment is declined | 4000000000009995 | ### Install the Stripe Node library Install the library: ``` npm install --save stripe ``` Initialize the Stripe library with your key: ### Install the Stripe Ruby library Install the gem: ``` gem install stripe ``` Initialize the Stripe library with your key: ### Install the Stripe Java library Add the following dependency to your POM and replace {VERSION} with the version number you want to use. ``` com.stripe stripe-java {VERSION} ``` Initialize the Stripe library with your key: ### Install the Stripe Python package Install the package via pip: ``` pip3 install stripe ``` Initialize the Stripe library with your key: ### Install the Stripe PHP library Install the library: ``` composer require stripe/stripe-php ``` Initialize the Stripe library with your key: ### Set up your server Make sure to initialize with Go Modules: ``` go get -u github.com/stripe/stripe-go/v82 ``` Initialize the Stripe library with your key: ### Install the Stripe.net library Install the library: ``` dotnet add package Stripe.net ``` Initialize the Stripe library with your key: ### Install the Stripe libraries Install the libraries: ``` npm install --save stripe @stripe/stripe-js next ``` Initialize the Stripe library with your key: ### Create a ConnectionToken endpoint To connect to a reader, your backend needs to give the SDK permission to use the reader with your Stripe account by providing it with the secret from a [ConnectionToken](https://docs.stripe.com/api/terminal/connection_tokens.md). Your backend should only create connection tokens for clients that it trusts. ​​If you’re using Stripe Connect, you should also [scope the connection token](https://docs.stripe.com/terminal/features/connect.md) to the relevant connected accounts. ​​If using locations, you should [pass a location ID](https://docs.stripe.com/terminal/fleet/locations-and-zones.md#connection-tokens) when creating the connection token to control access to readers. ### Organize your readers [Create locations](https://docs.stripe.com/terminal/fleet/locations-and-zones.md) to organize your readers. Locations group readers and allows them to automatically download the reader configuration needed for their region of use. ### Install the SDK The iOS SDK is [open source](https://github.com/stripe/stripe-terminal-ios), fully documented, and compatible with apps supporting iOS 10 or above. Import the Stripe SDK into your checkout screen’s UIViewController. Add this line to your Podfile, and use the .xcworkspace file to open your project in Xcode, instead of the .xcodeproj file, from here on out. ``` pod 'StripeTerminal', '~> 4.0' ``` ### Configure your app To prepare your app to work with the Stripe Terminal SDK, make a few changes to your Info.plist file in Xcode. Enable location services with the following key-value pair. ``` NSLocationWhenInUseUsageDescription Location access is required in order to accept payments. ``` ### Fetch ConnectionToken Implement the ConnectionTokenProvider protocol in your app, which defines a single function that requests a connection token from your backend. ### Initialize the SDK To get started, provide your ConnectionTokenProvider. You can only call `setTokenProvider` once in your app, and must call it before accessing `Terminal.shared`. ### Discover readers The Stripe Terminal SDK comes with a built-in simulated card reader, so you can develop and test your app without connecting to physical hardware. To use the simulated reader, call discoverReaders to search for readers, with the simulated option set to true. ### Discover readers The Stripe Terminal SDK comes with a built-in simulated card reader, so you can develop and test your app without connecting to physical hardware. To use the simulated reader, call `discoverReaders` to search for readers, with the simulated option set to true. You can discover intended readers more easily by [filtering by location](https://docs.stripe.com/terminal/fleet/register-readers.md). ### Connect to the simulated reader When the `didUpdateDiscoveredReaders` delegate method is called, call connectInternetReader to connect to the simulated reader. ### Connect to the simulated reader When the `didUpdateDiscoveredReaders` delegate method is called, call connectBluetoothReader to connect to the simulated reader. ### Create a PaymentIntent Create a [PaymentIntent](https://docs.stripe.com/api/payment_intents.md) object using the SDK. A PaymentIntent tracks the customer’s payment lifecycle, keeping track of any failed payment attempts and ensuring the customer is only charged once. ### Collect payment method details Call `collectPaymentMethod` with the PaymentIntent’s client secret to collect a payment method. When connected to the simulated reader calling this method immediately updates the PaymentIntent object with a [simulated test card](https://docs.stripe.com/terminal/references/testing.md#simulated-test-cards). When connected to a physical reader the connected reader waits for a card to be presented. ### Confirm the payment After successfully collecting payment method data, call `confirmPaymentIntent` with the updated PaymentIntent to confirm the payment. A successful call results in a PaymentIntent with a status of `requires_capture` for manual capture or `succeeded` for automatic capture. ### Create an endpoint to capture the PaymentIntent Create an endpoint on your backend that accepts a PaymentIntent ID and sends a request to the Stripe API to capture it. ### Capture the PaymentIntent If you defined `capture_method` as `manual` during PaymentIntent creation, the SDK returns an authorized but not captured PaymentIntent to your application. When the PaymentIntent status is `requires_capture`, notify your backend to capture the PaymentIntent. In your request send the PaymentIntent ID. To ensure the application fee captured is correct for connected accounts, inspect each `PaymentIntent` and modify the application fee, if needed, prior to manually capturing the payment. ### Run the application Run your server and go to [localhost:4242](http://localhost:4242). ### Make a test payment Use [amounts](https://docs.stripe.com/terminal/references/testing.md#physical-test-cards) ending in the following special values to test your integration. | Scenario | Card Number | | ------------------------------- | ---------------- | | Payment succeeds | 4242424242424242 | | Payment requires authentication | 4000002500003155 | | Payment is declined | 4000000000009995 | ### Install the Stripe Node library Install the library: ``` npm install --save stripe ``` Initialize the Stripe library with your key: ### Install the Stripe Ruby library Install the gem: ``` gem install stripe ``` Initialize the Stripe library with your key: ### Install the Stripe Java library Add the following dependency to your POM and replace {VERSION} with the version number you want to use. ``` com.stripe stripe-java {VERSION} ``` Initialize the Stripe library with your key: ### Install the Stripe Python package Install the package via pip: ``` pip3 install stripe ``` Initialize the Stripe library with your key: ### Install the Stripe PHP library Install the library: ``` composer require stripe/stripe-php ``` Initialize the Stripe library with your key: ### Set up your server Make sure to initialize with Go Modules: ``` go get -u github.com/stripe/stripe-go/v82 ``` Initialize the Stripe library with your key: ### Install the Stripe.net library Install the library: ``` dotnet add package Stripe.net ``` Initialize the Stripe library with your key: ### Install the Stripe libraries Install the libraries: ``` npm install --save stripe @stripe/stripe-js next ``` Initialize the Stripe library with your key: ### Create a ConnectionToken endpoint To connect to a reader, your backend needs to give the SDK permission to use the reader with your Stripe account by providing it with the secret from a [ConnectionToken](https://docs.stripe.com/api/terminal/connection_tokens.md). Your backend should only create connection tokens for clients that it trusts. ​​If you’re using Stripe Connect, you should also [scope the connection token](https://docs.stripe.com/terminal/features/connect.md) to the relevant connected accounts. ​​If using locations, you should [pass a location ID](https://docs.stripe.com/terminal/fleet/locations-and-zones.md#connection-tokens) when creating the connection token to control access to readers. ### Organize your readers [Create locations](https://docs.stripe.com/terminal/fleet/locations-and-zones.md) to organize your readers. Locations group readers and allows them to automatically download the reader configuration needed for their region of use. ### Install the SDK To install the SDK, add stripeterminal to the dependencies block of your build.gradle file. Add the dependencies to your build.gradle file: #### Groovy ```groovy dependencies { // ... // Stripe Terminal SDK implementation 'com.stripe:stripeterminal:4.3.1' } ``` #### Kotlin ```kotlin dependencies { // ... // Stripe Terminal SDK implementation("com.stripe:stripeterminal:4.3.1") } ``` ### Verify ACCESS_FINE_LOCATION permission Add a check to make sure that the `ACCESS_FINE_LOCATION` permission is enabled in your app. ### Verify user location permission Override the `onRequestPermissionsResult` method in your app and check the permission result to verify that the app user grants location permission. ### Fetch ConnectionToken Implement the ConnectionTokenProvider interface in your app, which defines a single function that requests a connection token from your backend. ### Configure TerminalApplicationDelegate To prevent memory leaks and ensure proper cleanup of long-running Terminal SDK processes, your application must subclass `Application` and call out to the `TerminalApplicationDelegate` from the `onCreate` method. ### Initialize the SDK To get started, provide the current application context, the ConnectionTokenProvider, and a TerminalListener object. ### Discover readers The Stripe Terminal SDK comes with a built-in simulated card reader, so you can develop and test your app without connecting to physical hardware. To use the simulated reader, call discoverReaders to search for readers, with the simulated option set to true. ### Discover readers The Stripe Terminal SDK comes with a built-in simulated card reader, so you can develop and test your app without connecting to physical hardware. To use the simulated reader, call `discoverReaders` to search for readers, with the simulated option set to true. You can discover intended readers more easily by [filtering by location](https://docs.stripe.com/terminal/fleet/register-readers.md). ### Connect to the simulated reader When `discoverReaders` returns a result, call `connectInternetReader` to connect to the simulated reader. ### Connect to the simulated reader When `discoverReaders` returns a result, call `connectBluetoothReader` to connect to the simulated reader. ### Create a PaymentIntent Create a [PaymentIntent](https://docs.stripe.com/api/payment_intents.md) object using the SDK. A PaymentIntent tracks the customer’s payment lifecycle, keeping track of any failed payment attempts and ensuring the customer is only charged once. ### Collect payment method details Call `collectPaymentMethod` with the PaymentIntent’s client secret to collect a payment method. When connected to the simulated reader calling this method immediately updates the PaymentIntent object with a [simulated test card](https://docs.stripe.com/terminal/references/testing.md#simulated-test-cards). When connected to a physical reader the connected reader waits for a card to be presented. ### Confirm the payment After successfully collecting payment method data, call `confirmPaymentIntent` with the updated PaymentIntent to confirm the payment. A successful call results in a PaymentIntent with a status of `requires_capture` for manual capture or `succeeded` for automatic capture. ### Create an endpoint to capture the PaymentIntent Create an endpoint on your backend that accepts a PaymentIntent ID and sends a request to the Stripe API to capture it. ### Capture the PaymentIntent If you defined `capture_method` as `manual` during PaymentIntent creation, the SDK returns an authorized but not captured PaymentIntent to your application. When the PaymentIntent status is `requires_capture`, notify your backend to capture the PaymentIntent. In your request send the PaymentIntent ID. To ensure the application fee captured is correct for connected accounts, inspect each `PaymentIntent` and modify the application fee, if needed, prior to manually capturing the payment. ### Run the application Run your server and go to [localhost:4242](http://localhost:4242). ### Make a test payment Use [amounts](https://docs.stripe.com/terminal/references/testing.md#physical-test-cards) ending in the following special values to test your integration. | Scenario | Card Number | | ------------------------------- | ---------------- | | Payment succeeds | 4242424242424242 | | Payment requires authentication | 4000002500003155 | | Payment is declined | 4000000000009995 | ### Install the Stripe Node library Install the library: ``` npm install --save stripe ``` Initialize the Stripe library with your key: ### Install the Stripe Ruby library Install the gem: ``` gem install stripe ``` Initialize the Stripe library with your key: ### Install the Stripe Java library Add the following dependency to your POM and replace {VERSION} with the version number you want to use. ``` com.stripe stripe-java {VERSION} ``` Initialize the Stripe library with your key: ### Install the Stripe Python package Install the package via pip: ``` pip3 install stripe ``` Initialize the Stripe library with your key: ### Install the Stripe PHP library Install the library: ``` composer require stripe/stripe-php ``` Initialize the Stripe library with your key: ### Set up your server Make sure to initialize with Go Modules: ``` go get -u github.com/stripe/stripe-go/v82 ``` Initialize the Stripe library with your key: ### Install the Stripe.net library Install the library: ``` dotnet add package Stripe.net ``` Initialize the Stripe library with your key: ### Install the Stripe libraries Install the libraries: ``` npm install --save stripe @stripe/stripe-js next ``` Initialize the Stripe library with your key: ### Create a ConnectionToken endpoint To connect to a reader, your backend needs to give the SDK permission to use the reader with your Stripe account by providing it with the secret from a [ConnectionToken](https://docs.stripe.com/api/terminal/connection_tokens.md). Your backend should only create connection tokens for clients that it trusts. ​​If you’re using Stripe Connect, you should also [scope the connection token](https://docs.stripe.com/terminal/features/connect.md) to the relevant connected accounts. ​​If using locations, you should [pass a location ID](https://docs.stripe.com/terminal/fleet/locations-and-zones.md#connection-tokens) when creating the connection token to control access to readers. ### Organize your readers [Create locations](https://docs.stripe.com/terminal/fleet/locations-and-zones.md) to organize your readers. Locations group readers and allows them to automatically download the reader configuration needed for their region of use. ### Install the SDK ``` yarn install @stripe/stripe-terminal-react-native ``` ### Fetch ConnectionToken To give the SDK access to this endpoint, create a function in your web application that requests a ConnectionToken from your backend and returns the secret from the ConnectionToken object. ### Configure permissions Add a check to make sure that the `ACCESS_FINE_LOCATION` permission is enabled in your app. ### Set up the context provider Pass the `onFetchConnectionToken` function to `StripeTerminalProvider` as a prop. ### Initialize the SDK To initialize a `StripeTerminal` instance in your React Native application, call the initialize method from `useStripeTerminal` hook. You must call the `initialize` method from a component nested within `StripeTerminalProvider` and not from the component that contains the `StripeTerminalProvider`. ### Discover readers The Stripe Terminal SDK comes with a built-in simulated card reader, so you can develop and test your app without connecting to physical hardware. To use the simulated reader, call `discoverReaders` to search for readers, with the simulated option set to true. ### Discover readers The Stripe Terminal SDK comes with a built-in simulated card reader, so you can develop and test your app without connecting to physical hardware. To use the simulated reader, call `discoverReaders` to search for readers, with the simulated option set to true. You can discover intended readers more easily by [filtering by location](https://docs.stripe.com/terminal/fleet/register-readers.md). ### Connect to the simulated reader When `discoverReaders` returns a result, call `connectReader` to connect to the simulated reader. ### Connect to the simulated reader When `discoverReaders` returns a result, call `connectBluetoothReader` to connect to the simulated reader. ### Create a PaymentIntent Add an endpoint on your server that creates a PaymentIntent. A PaymentIntent tracks the customer’s payment lifecycle, keeping track of any failed payment attempts and ensuring they’re only charged once. Return the PaymentIntent’s client secret in the response. If you’re using Stripe Connect, you can also specify [connected account information](https://docs.stripe.com/terminal/features/connect.md) based on your platform’s charge logic. ### Fetch the PaymentIntent Make a request to your server for a PaymentIntent to initiate the payment process. ### Collect payment method details Call `collectPaymentMethod` with the PaymentIntent’s client secret to collect a payment method. When connected to the simulated reader calling this method immediately updates the PaymentIntent object with a [simulated test card](https://docs.stripe.com/terminal/references/testing.md#simulated-test-cards). When connected to a physical reader the connected reader waits for a card to be presented. ### Process the payment After successfully collecting payment method data, call `processPayment` with the updated PaymentIntent to process the payment. A successful call results in a PaymentIntent with a status of `requires_capture` for manual capture or `succeeded` for automatic capture. ### Create an endpoint to capture the PaymentIntent Create an endpoint on your backend that accepts a PaymentIntent ID and sends a request to the Stripe API to capture it. ### Capture the PaymentIntent If you defined `capture_method` as `manual` during PaymentIntent creation, the SDK returns an authorized but not captured PaymentIntent to your application. When the PaymentIntent status is `requires_capture`, notify your backend to capture the PaymentIntent. In your request send the PaymentIntent ID. ### Run the application Run your server and go to [localhost:4242](http://localhost:4242). ### Use a test card number to try your integration The simulated reader supports a small amount of configuration, enabling you to test different flows within your point of sale application such as different card brands or error scenarios like a declined charge. To enable this behavior, insert this line of code before you call `collectPaymentMethod.` | Scenario | Card Number | | ------------------------------- | ---------------- | | Payment succeeds | 4242424242424242 | | Payment requires authentication | 4000002500003155 | | Payment is declined | 4000000000009995 | ```javascript const express = require("express"); const app = express(); const { resolve } = require("path"); const stripe = require("stripe")("<>"); app.use(express.static("public")); app.use(express.json()); app.use(express.urlencoded({ extended: true })); const createLocation = async () => { const location = await stripe.terminal.locations.create({ display_name: '{{TERMINAL_LOCATION_NAME}}', address: { line1: '{{TERMINAL_LOCATION_LINE1}}', line2: '{{TERMINAL_LOCATION_LINE2}}', city: '{{TERMINAL_LOCATION_CITY}}', state: '{{TERMINAL_LOCATION_STATE}}', country: '{{TERMINAL_LOCATION_COUNTRY}}', postal_code: '{{TERMINAL_LOCATION_POSTAL}}', } }); return location; }; app.post("/create_location", async (req, res) => { const location = await stripe.terminal.locations.create({ display_name: '{{TERMINAL_LOCATION_NAME}}', address: { line1: '{{TERMINAL_LOCATION_LINE1}}', line2: '{{TERMINAL_LOCATION_LINE2}}', city: '{{TERMINAL_LOCATION_CITY}}', state: '{{TERMINAL_LOCATION_STATE}}', country: '{{TERMINAL_LOCATION_COUNTRY}}', postal_code: '{{TERMINAL_LOCATION_POSTAL}}', } }); res.json(location); }); app.post("/register_reader", async (req, res) => { const reader = await stripe.terminal.readers.create({ registration_code: 'simulated-wpe', location: req.body.location_id, }); res.json(reader); }); // The ConnectionToken's secret lets you connect to any Stripe Terminal reader // and take payments with your Stripe account. // Be sure to authenticate the endpoint for creating connection tokens. app.post("/connection_token", async (req, res) => { let connectionToken = await stripe.terminal.connectionTokens.create(); res.json({secret: connectionToken.secret}); }) app.post("/create_payment_intent", async (req, res) => { // For Terminal payments, the 'payment_method_types' parameter must include // 'card_present'. // To automatically capture funds when a charge is authorized, // set `capture_method` to `automatic`. const intent = await stripe.paymentIntents.create({ amount: req.body.amount, currency: '{{TERMINAL_CURRENCY}}', payment_method_types: [ '{{TERMINAL_PAYMENT_METHODS}}' ], capture_method: 'manual', }); res.json(intent); }); app.post("/process_payment", async (req, res) => { var attempt = 0; const tries = 3; while (true) { attempt++; try { const reader = await stripe.terminal.readers.processPaymentIntent( req.body.reader_id, { payment_intent: req.body.payment_intent_id, } ); return res.send(reader); } catch (error) { console.log(error); switch (error.code) { case "terminal_reader_timeout": // Temporary networking blip, automatically retry a few times. if (attempt == tries) { return res.send(error); } break; case "terminal_reader_offline": // Reader is offline and won't respond to API requests. Make sure the reader is powered on // and connected to the internet before retrying. return res.send(error); case "terminal_reader_busy": // Reader is currently busy processing another request, installing updates or changing settings. // Remember to disable the pay button in your point-of-sale application while waiting for a // reader to respond to an API request. return res.send(error); case "intent_invalid_state": // Check PaymentIntent status because it's not ready to be processed. It might have been already // successfully processed or canceled. const paymentIntent = await stripe.paymentIntents.retrieve( req.body.payment_intent_id ); console.log( "PaymentIntent is already in " + paymentIntent.status + " state." ); return res.send(error); default: return res.send(error); } } } }); app.post("/simulate_payment", async (req, res) => { const reader = await stripe.testHelpers.terminal.readers.presentPaymentMethod(req.body.reader_id); res.send(reader); }); app.post("/capture_payment_intent", async (req, res) => { const intent = await stripe.paymentIntents.capture(req.body.payment_intent_id); res.send(intent); }); app.listen(4242, () => console.log('Node server listening on port 4242!')); ``` ```json { "name": "stripe-sample", "version": "1.0.0", "description": "A sample Stripe implementation", "main": "server.js", "scripts": { "start": "node server.js" }, "author": "stripe-samples", "license": "ISC", "dependencies": { "express": "^4.17.1", "stripe": "^18.0.0" } } { "name": "stripe-sample", "version": "0.1.0", "dependencies": { "@stripe/react-stripe-js": "^1.0.0", "@stripe/stripe-js": "^1.0.0", "express": "^4.17.1", "react": "^16.9.0", "react-dom": "^16.9.0", "react-scripts": "^3.4.0", "stripe": "^18.0.0" }, "devDependencies": { "concurrently": "4.1.2" }, "homepage": "http://localhost:3000/checkout", "proxy": "http://localhost:4242", "scripts": { "start-client": "react-scripts start", "start-server": "node server.js", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject", "start": "concurrently \"yarn start-client\" \"yarn start-server\"" }, "eslintConfig": { "extends": "react-app" }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] } } ``` ```ruby require 'sinatra' require 'stripe' Stripe.api_key = '<>' set :root, File.dirname(__FILE__) set :public_folder, -> { File.join(root, 'public') } set :static, true set :port, 4242 def create_location Stripe::Terminal::Location.create({ display_name: '{{TERMINAL_LOCATION_NAME}}', address: { line1: '{{TERMINAL_LOCATION_LINE1}}', line2: '{{TERMINAL_LOCATION_LINE2}}', city: '{{TERMINAL_LOCATION_CITY}}', state: '{{TERMINAL_LOCATION_STATE}}', country: '{{TERMINAL_LOCATION_COUNTRY}}', postal_code: '{{TERMINAL_LOCATION_POSTAL}}', } }) end get '/' do redirect '/index.html' end post '/create_location' do content_type 'application/json' location = Stripe::Terminal::Location.create({ display_name: '{{TERMINAL_LOCATION_NAME}}', address: { line1: '{{TERMINAL_LOCATION_LINE1}}', line2: '{{TERMINAL_LOCATION_LINE2}}', city: '{{TERMINAL_LOCATION_CITY}}', state: '{{TERMINAL_LOCATION_STATE}}', country: '{{TERMINAL_LOCATION_COUNTRY}}', postal_code: '{{TERMINAL_LOCATION_POSTAL}}', } }) location.to_json end post '/register_reader' do content_type 'application/json' data = JSON.parse(request.body.read) reader = Stripe::Terminal::Reader.create( location: data['location_id'], registration_code: 'simulated-wpe' ) reader.to_json end # The ConnectionToken's secret lets you connect to any Stripe Terminal reader # and take payments with your Stripe account. # Be sure to authenticate the endpoint for creating connection tokens. post '/connection_token' do content_type 'application/json' connection_token = Stripe::Terminal::ConnectionToken.create {secret: connection_token.secret}.to_json end post '/create_payment_intent' do content_type 'application/json' data = JSON.parse(request.body.read) # For Terminal payments, the 'payment_method_types' parameter must include # 'card_present'. # To automatically capture funds when a charge is authorized, # set `capture_method` to `automatic`. intent = Stripe::PaymentIntent.create( amount: data['amount'], currency: '{{TERMINAL_CURRENCY}}', payment_method_types: [ '{{TERMINAL_PAYMENT_METHODS}}' ], capture_method: 'manual', ) intent.to_json end post '/process_payment' do content_type 'application/json' data = JSON.parse(request.body.read) tries = 0 begin tries += 1 reader = Stripe::Terminal::Reader.process_payment_intent( data['reader_id'], payment_intent: data['payment_intent_id'] ) reader.to_json rescue Stripe::InvalidRequestError => e case e.code when 'terminal_reader_timeout' # Temporary networking blip, automatically retry a few times. retry if tries < 3 when 'terminal_reader_offline' # Reader is offline and won't respond to API requests. Make sure the reader is powered on # and connected to the internet before retrying. request.logger.error(e.message) when 'terminal_reader_busy' # Reader is currently busy processing another request, installing updates, or changing settings. # Remember to disable the pay button in your point-of-sale application while waiting for a # reader to respond to an API request. request.logger.error(e.message) when 'intent_invalid_state' # Check PaymentIntent status because it's not ready to be processed. It might have been already # successfully processed or canceled. payment_intent = Stripe::PaymentIntent.retrieve(data['payment_intent_id']) request.logger.error("PaymentIntent is already in #{payment_intent.status} state.") else request.logger.error(e.message) end e.to_json end end post '/simulate_payment' do content_type 'application/json' data = JSON.parse(request.body.read) reader = Stripe::Terminal::Reader::TestHelpers.present_payment_method( data['reader_id'] ) reader.to_json end post '/capture_payment_intent' do data = JSON.parse(request.body.read) intent = Stripe::PaymentIntent.capture(data['payment_intent_id']) intent.to_json end ``` ``` source 'https://rubygems.org/' gem 'sinatra' gem 'stripe' ``` ```python \#! /usr/bin/env python3.6 # Python 3.6 or newer required. import json import os import stripe stripe.api_key = '<>' from flask import Flask, jsonify, request, render_template app = Flask(__name__, static_folder='public', static_url_path='', template_folder='public') @app.route('/') def index(): return render_template('index.html') def create_location(): location = stripe.terminal.Location.create( display_name='{{TERMINAL_LOCATION_NAME}}', address={ 'line1': '{{TERMINAL_LOCATION_LINE1}}', 'line2': '{{TERMINAL_LOCATION_LINE2}}', 'city': '{{TERMINAL_LOCATION_CITY}}', 'state': '{{TERMINAL_LOCATION_STATE}}', 'country': '{{TERMINAL_LOCATION_COUNTRY}}', 'postal_code': '{{TERMINAL_LOCATION_POSTAL}}', }, ) return location @app.route('/create_location', methods=['POST']) def create_location(): location = stripe.terminal.Location.create( display_name='{{TERMINAL_LOCATION_NAME}}', address={ 'line1': '{{TERMINAL_LOCATION_LINE1}}', 'line2': '{{TERMINAL_LOCATION_LINE2}}', 'city': '{{TERMINAL_LOCATION_CITY}}', 'state': '{{TERMINAL_LOCATION_STATE}}', 'country': '{{TERMINAL_LOCATION_COUNTRY}}', 'postal_code': '{{TERMINAL_LOCATION_POSTAL}}', }, ) return location @app.route('/register_reader', methods=['POST']) def register_reader(): data = json.loads(request.data) reader = stripe.terminal.Reader.create( location=data['location_id'], registration_code='simulated-wpe', ) return reader @app.route('/create_payment_intent', methods=['POST']) def secret(): data = json.loads(request.data) # For Terminal payments, the 'payment_method_types' parameter must include # 'card_present'. # To automatically capture funds when a charge is authorized, # set `capture_method` to `automatic`. intent = stripe.PaymentIntent.create( amount=data['amount'], currency='{{TERMINAL_CURRENCY}}', payment_method_types=[ '{{TERMINAL_PAYMENT_METHODS}}' ], capture_method='manual' ) return intent @app.route('/process_payment', methods=['POST']) def process_payment(): data = json.loads(request.data) tries = 3 for attempt in range(tries): try: reader = stripe.terminal.Reader.process_payment_intent( data['reader_id'], payment_intent=data['payment_intent_id'], ) return reader except stripe.error.InvalidRequestError as e: if e.code == 'terminal_reader_timeout': # Temporary networking blip, automatically retry a few times. if attempt < tries - 1: continue else: return e.json_body elif e.code == 'terminal_reader_offline': # Reader is offline and won't respond to API requests. Make sure the reader is powered on # and connected to the internet before retrying. app.logger.error(e) return e.json_body elif e.code == 'terminal_reader_busy': # Reader is currently busy processing another request, installing updates or changing settings. # Remember to disable the pay button in your point-of-sale application while waiting for a # reader to respond to an API request. app.logger.error(e) return e.json_body elif e.code == 'intent_invalid_state': # Check PaymentIntent status because it's not ready to be processed. It might have been already # successfully processed or canceled. payment_intent = stripe.PaymentIntent.retrieve(data['payment_intent_id']) app.logger.error('PaymentIntent is already in %s state.' % payment_intent.status) return e.json_body else: app.logger.error(e) return e.json_body @app.route('/simulate_payment', methods=['POST']) def simulate_payment(): data = json.loads(request.data) reader = stripe.terminal.Reader.TestHelpers.present_payment_method( data['reader_id'] ) return reader # The ConnectionToken's secret lets you connect to any Stripe Terminal reader # and take payments with your Stripe account. # Be sure to authenticate the endpoint for creating connection tokens. @app.route('/connection_token', methods=['POST']) def token(): connection_token = stripe.terminal.ConnectionToken.create() return jsonify(secret=connection_token.secret) @app.route('/capture_payment_intent', methods=['POST']) def capture(): data = json.loads(request.data) intent = stripe.PaymentIntent.capture( data['payment_intent_id'] ) return intent if __name__ == '__main__': app.run() ``` ``` certifi==2021.5.30 chardet==4.0.0 Click==8.0.1 Flask==2.0.1 idna==3.2 itsdangerous==2.0.1 Jinja2==3.0.1 MarkupSafe==2.0.1 requests==2.26.0 stripe==12.0.0 toml==0.10.2 Werkzeug==2.0.1 ``` ```php >'); try { $location = $stripe->terminal->locations->create([ 'display_name' => '{{TERMINAL_LOCATION_NAME}}', 'address' => [ 'line1' => "{{TERMINAL_LOCATION_LINE1}}", 'line2' => "{{TERMINAL_LOCATION_LINE2}}", 'city' => "{{TERMINAL_LOCATION_CITY}}", 'state' => "{{TERMINAL_LOCATION_STATE}}", 'country' => "{{TERMINAL_LOCATION_COUNTRY}}", 'postal_code' => "{{TERMINAL_LOCATION_POSTAL}}", ] ]); echo json_encode($location); } catch (Throwable $e) { http_response_code(500); echo json_encode(['error' => $e->getMessage()]); } ``` ```php >'); try { $json_str = file_get_contents('php://input'); $json_obj = json_decode($json_str); $reader = $stripe->terminal->readers->create([ 'location' => $json_obj->location_id, 'registration_code' => 'simulated-wpe', ]); echo json_encode($reader); } catch (Throwable $e) { http_response_code(500); echo json_encode(['error' => $e->getMessage()]); } ``` ```php >'); try { $json_str = file_get_contents('php://input'); $json_obj = json_decode($json_str); // For Terminal payments, the 'payment_method_types' parameter must include // 'card_present'. // To automatically capture funds when a charge is authorized, // set `capture_method` to `automatic`. $intent = $stripe->paymentIntents->create([ 'amount' => $json_obj->amount, 'currency' => '{{TERMINAL_CURRENCY}}', 'payment_method_types' => [ '{{TERMINAL_PAYMENT_METHODS}}' ], 'capture_method' => 'manual' ]); echo json_encode($intent); } catch (Throwable $e) { http_response_code(500); echo json_encode(['error' => $e->getMessage()]); } ``` ```php >'); $attempt = 0; $tries = 3; $shouldRetry = false; $json_str = file_get_contents('php://input'); $json_obj = json_decode($json_str); do { $attempt++; try { $reader = $stripe->terminal->readers->processPaymentIntent($json_obj->reader_id, [ 'payment_intent' => $json_obj->payment_intent_id, ]); echo json_encode($reader); } catch (\Stripe\Exception\InvalidRequestException $e) { switch($e->getStripeCode()) { case "terminal_reader_timeout": // Temporary networking blip, automatically retry a few times. if ($attempt == $tries) { $shouldRetry = false; echo json_encode(['error' => $e->getMessage()]); } else { $shouldRetry = true; } break; case "terminal_reader_offline": // Reader is offline and won't respond to API requests. Make sure the reader is powered on // and connected to the internet before retrying. $shouldRetry = false; echo json_encode(['error' => $e->getMessage()]); break; case "terminal_reader_busy": // Reader is currently busy processing another request, installing updates or changing settings. // Remember to disable the pay button in your point-of-sale application while waiting for a // reader to respond to an API request. $shouldRetry = false; echo json_encode(['error' => $e->getMessage()]); break; case "intent_invalid_state": // Check PaymentIntent status because it's not ready to be processed. It might have been already // successfully processed or canceled. $shouldRetry = false; $paymentIntent = $stripe->paymentIntents->retrieve($json_obj->payment_intent_id); echo json_encode(['error' => 'PaymentIntent is already in ' . $paymentIntent->status . ' state.']); break; default: $shouldRetry = false; echo json_encode(['error' => $e->getMessage()]); break; } } } while($shouldRetry); ``` ```php >'); try { $json_str = file_get_contents('php://input'); $json_obj = json_decode($json_str); $reader = $stripe->testHelpers->terminal->readers->presentPaymentMethod($json_obj->reader_id); echo json_encode($reader); } catch (Throwable $e) { http_response_code(500); echo json_encode(['error' => $e->getMessage()]); } ``` ```php >'); try { $json_str = file_get_contents('php://input'); $json_obj = json_decode($json_str); $intent = $stripe->paymentIntents->capture($json_obj->payment_intent_id); echo json_encode($intent); } catch (Throwable $e) { http_response_code(500); echo json_encode(['error' => $e->getMessage()]); } ``` ```php >'); try { // The ConnectionToken's secret lets you connect to any Stripe Terminal reader // and take payments with your Stripe account. // Be sure to authenticate the endpoint for creating connection tokens. $connectionToken = $stripe->terminal->connectionTokens->create(); echo json_encode(array('secret' => $connectionToken->secret)); } catch (Throwable $e) { http_response_code(500); echo json_encode(['error' => $e->getMessage()]); } ``` ```json { "require": { "stripe/stripe-php": "^17.0" } } ``` ```csharp using System; using System.Collections.Generic; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Newtonsoft.Json; using Stripe; using Stripe.Terminal; namespace StripeExample { public class Program { public static void Main(string[] args) { WebHost.CreateDefaultBuilder(args) .UseUrls("http://0.0.0.0:4242") .UseWebRoot("public") .UseStartup() .Build() .Run(); } private static Location createLocation(){ var options = new LocationCreateOptions { DisplayName = "{{TERMINAL_LOCATION_NAME}}", Address = new AddressOptions { Line1 = "{{TERMINAL_LOCATION_LINE1}}", Line2 = "{{TERMINAL_LOCATION_LINE2}}", City = "{{TERMINAL_LOCATION_CITY}}", State = "{{TERMINAL_LOCATION_STATE}}", Country = "{{TERMINAL_LOCATION_COUNTRY}}", PostalCode = "{{TERMINAL_LOCATION_POSTAL}}", }, }; var service = new LocationService(); var location = service.Create(options); return location; } } public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddMvc().AddNewtonsoftJson(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { StripeConfiguration.ApiKey = "<>"; if (env.IsDevelopment()) app.UseDeveloperExceptionPage(); app.UseRouting(); app.UseDefaultFiles(); app.UseStaticFiles(); app.UseEndpoints(endpoints => endpoints.MapControllers()); } } // The ConnectionToken's secret lets you connect to any Stripe Terminal reader // and take payments with your Stripe account. // Be sure to authenticate the endpoint for creating connection tokens. [Route("connection_token")] [ApiController] public class ConnectionTokenApiController : Controller { [HttpPost] public ActionResult Post() { var options = new ConnectionTokenCreateOptions{}; var service = new ConnectionTokenService(); var connectionToken = service.Create(options); return Json(new {secret = connectionToken.Secret}); } } [Route("create_location")] [ApiController] public class CreateLocationApiController : Controller { [HttpPost] public ActionResult Post() { var options = new LocationCreateOptions { DisplayName = "{{TERMINAL_LOCATION_NAME}}", Address = new AddressOptions { Line1 = "{{TERMINAL_LOCATION_LINE1}}", City = "{{TERMINAL_LOCATION_CITY}}", State = "{{TERMINAL_LOCATION_STATE}}", Country = "{{TERMINAL_LOCATION_COUNTRY}}", PostalCode = "{{TERMINAL_LOCATION_POSTAL}}", }, }; var service = new LocationService(); var location = service.Create(options); return Json(location); } } [Route("register_reader")] [ApiController] public class RegisterReaderApiController : Controller { [HttpPost] public ActionResult Post(RegisterReaderRequest request) { var options = new ReaderCreateOptions { RegistrationCode = "simulated-wpe", Location = request.LocationId, }; var service = new ReaderService(); var reader = service.Create(options); return Json(reader); } } public class RegisterReaderRequest { [JsonProperty("location_id")] public string LocationId { get; set; } } [Route("create_payment_intent")] [ApiController] public class PaymentIntentApiController : Controller { [HttpPost] public ActionResult Post(PaymentIntentCreateRequest request) { var service = new PaymentIntentService(); // For Terminal payments, the 'payment_method_types' parameter must include // 'card_present'. // To automatically capture funds when a charge is authorized, // set `capture_method` to `automatic`. var options = new PaymentIntentCreateOptions { Amount = long.Parse(request.Amount), Currency = "{{TERMINAL_CURRENCY}}", PaymentMethodTypes = new List { "{{TERMINAL_PAYMENT_METHODS}}" }, CaptureMethod = "manual", }; var intent = service.Create(options); return Json(intent); } public class PaymentIntentCreateRequest { [JsonProperty("amount")] public string Amount { get; set; } } } [Route("process_payment")] [ApiController] public class ProcessPaymentApiController : Controller { [HttpPost] public ActionResult Post(ProcessPaymentRequest request) { var service = new ReaderService(); var options = new ReaderProcessPaymentIntentOptions { PaymentIntent = request.PaymentIntentId, }; var attempt = 0; var tries = 3; while (true) { attempt++; try { var reader = service.ProcessPaymentIntent(request.ReaderId, options); return Json(reader); } catch (StripeException e) { switch (e.StripeError.Code) { case "terminal_reader_timeout": // Temporary networking blip, automatically retry a few times. if (attempt == tries) { return Json(e.StripeError); } break; case "terminal_reader_offline": // Reader is offline and won't respond to API requests. Make sure the reader is powered on // and connected to the internet before retrying. return Json(e.StripeError); case "terminal_reader_busy": // Reader is currently busy processing another request, installing updates or changing settings. // Remember to disable the pay button in your point-of-sale application while waiting for a // reader to respond to an API request. return Json(e.StripeError); case "intent_invalid_state": // Check PaymentIntent status because it's not ready to be processed. It might have been already // successfully processed or canceled. var paymentIntentService = new PaymentIntentService(); var paymentIntent = paymentIntentService.Get(request.PaymentIntentId); Console.WriteLine($"PaymentIntent is already in {paymentIntent.Status} state."); return Json(e.StripeError); default: return Json(e.StripeError); } } } } public class ProcessPaymentRequest { [JsonProperty("reader_id")] public string ReaderId { get; set; } [JsonProperty("payment_intent_id")] public string PaymentIntentId { get; set; } } } [Route("simulate_payment")] [ApiController] public class SimulatePaymentApiController : Controller { [HttpPost] public ActionResult Post(SimulatePaymentRequest request) { var service = new Stripe.TestHelpers.Terminal.ReaderService(); var reader = service.PresentPaymentMethod(request.ReaderId); return Json(reader); } public class SimulatePaymentRequest { [JsonProperty("reader_id")] public string ReaderId { get; set; } } } [Route("capture_payment_intent")] [ApiController] public class CapturePaymentIntentApiController : Controller { [HttpPost] public ActionResult Post(PaymentIntentCaptureRequest request) { var service = new PaymentIntentService(); var intent = service.Capture(request.PaymentIntentId, null); return Json(intent); } public class PaymentIntentCaptureRequest { [JsonProperty("payment_intent_id")] public string PaymentIntentId { get; set; } } } } ``` ``` net8.0 StripeExample Major ``` ```go package main import ( "bytes" "encoding/json" "io" "log" "net/http" "strconv" "github.com/stripe/stripe-go/v82" "github.com/stripe/stripe-go/v82/paymentintent" "github.com/stripe/stripe-go/v82/terminal/connectiontoken" "github.com/stripe/stripe-go/v82/terminal/location" "github.com/stripe/stripe-go/v82/terminal/reader" readertesthelpers "github.com/stripe/stripe-go/v82/testhelpers/terminal/reader" ) func main() { stripe.Key = "<>" fs := http.FileServer(http.Dir("public")) http.Handle("/", fs) http.HandleFunc("/connection_token", handleConnectionToken) http.HandleFunc("/create_location", handleCreateLocation) http.HandleFunc("/register_reader", handleRegisterReader) http.HandleFunc("/process_payment", handleProcessPayment) http.HandleFunc("/simulate_payment", handleSimulatePayment) http.HandleFunc("/create_payment_intent", handleCreate) http.HandleFunc("/capture_payment_intent", handleCapture) addr := "localhost:4242" log.Printf("Listening on %s ...", addr) log.Fatal(http.ListenAndServe(addr, nil)) } func createLocation(w http.ResponseWriter, r *http.Request) *stripe.TerminalLocation { params := &stripe.TerminalLocationParams{ Address: &stripe.AddressParams{ Line1: stripe.String("{{TERMINAL_LOCATION_LINE1}}"), Line2: stripe.String("{{TERMINAL_LOCATION_LINE2}}"), City: stripe.String("{{TERMINAL_LOCATION_CITY}}"), State: stripe.String("{{TERMINAL_LOCATION_STATE}}"), Country: stripe.String("{{TERMINAL_LOCATION_COUNTRY}}"), PostalCode: stripe.String("{{TERMINAL_LOCATION_POSTAL}}"), }, DisplayName: stripe.String("{{TERMINAL_LOCATION_NAME}}"), } l, _ := location.New(params) return l } func handleCreateLocation(w http.ResponseWriter, r *http.Request) { params := &stripe.TerminalLocationParams{ Address: &stripe.AddressParams{ Line1: stripe.String("{{TERMINAL_LOCATION_LINE1}}"), Line2: stripe.String("{{TERMINAL_LOCATION_LINE2}}"), City: stripe.String("{{TERMINAL_LOCATION_CITY}}"), State: stripe.String("{{TERMINAL_LOCATION_STATE}}"), Country: stripe.String("{{TERMINAL_LOCATION_COUNTRY}}"), PostalCode: stripe.String("{{TERMINAL_LOCATION_POSTAL}}"), }, DisplayName: stripe.String("{{TERMINAL_LOCATION_NAME}}"), } l, err := location.New(params) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) log.Printf("location.New: %v", err) return } writeJSON(w, l) } func handleRegisterReader(w http.ResponseWriter, r *http.Request) { var req struct { LocationID string `json:"location_id"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) log.Printf("json.NewDecoder.Decode: %v", err) return } params := &stripe.TerminalReaderParams{ Location: stripe.String(req.LocationID), RegistrationCode: stripe.String("simulated-wpe"), } reader, err := reader.New(params) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) log.Printf("reader.New: %v", err) return } writeJSON(w, reader) } // The ConnectionToken's secret lets you connect to any Stripe Terminal reader // and take payments with your Stripe account. // Be sure to authenticate the endpoint for creating connection tokens. func handleConnectionToken(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } params := &stripe.TerminalConnectionTokenParams{} ct, err := connectiontoken.New(params) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) log.Printf("pi.New: %v", err) return } writeJSON(w, struct { Secret string `json:"secret"` }{ Secret: ct.Secret, }) } func handleCreate(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } var req struct { PaymentIntentAmount string `json:"amount"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) log.Printf("json.NewDecoder.Decode: %v", err) return } amount, _ := strconv.ParseInt(req.PaymentIntentAmount, 10, 64) // For Terminal payments, the 'payment_method_types' parameter must include // 'card_present'. // To automatically capture funds when a charge is authorized, // set `capture_method` to `automatic`. params := &stripe.PaymentIntentParams{ Amount: stripe.Int64(amount), Currency: stripe.String(string(stripe.Currency{{TERMINAL_CURRENCY}})), PaymentMethodTypes: stripe.StringSlice([]string{ "{{TERMINAL_PAYMENT_METHODS}}" }), CaptureMethod: stripe.String("manual"), } pi, err := paymentintent.New(params) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) log.Printf("pi.New: %v", err) return } writeJSON(w, pi) } func handleProcessPayment(w http.ResponseWriter, r *http.Request) { var req struct { ReaderID string `json:"reader_id"` PaymentIntentID string `json:"payment_intent_id"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) log.Printf("json.NewDecoder.Decode: %v", err) return } params := &stripe.TerminalReaderProcessPaymentIntentParams{ PaymentIntent: stripe.String(req.PaymentIntentID), } reader, err := reader.ProcessPaymentIntent(req.ReaderID, params) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) log.Printf("reader.New: %v", err) return } writeJSON(w, reader) } func handleSimulatePayment(w http.ResponseWriter, r *http.Request) { var req struct { ReaderID string `json:"reader_id"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) log.Printf("json.NewDecoder.Decode: %v", err) return } params := &stripe.TestHelpersTerminalReaderPresentPaymentMethodParams{} reader, err := readertesthelpers.PresentPaymentMethod(req.ReaderID, params) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) log.Printf("reader.New: %v", err) return } writeJSON(w, reader) } func handleCapture(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } var req struct { PaymentIntentID string `json:"payment_intent_id"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) log.Printf("json.NewDecoder.Decode: %v", err) return } pi, err := paymentintent.Capture(req.PaymentIntentID, nil) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) log.Printf("pi.Capture: %v", err) return } writeJSON(w, pi) } func writeJSON(w http.ResponseWriter, v interface{}) { var buf bytes.Buffer if err := json.NewEncoder(&buf).Encode(v); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) log.Printf("json.NewEncoder.Encode: %v", err) return } w.Header().Set("Content-Type", "application/json") if _, err := io.Copy(w, &buf); err != nil { log.Printf("io.Copy: %v", err) return } } ``` ``` module stripe.com/docs/payments go 1.13 require github.com/stripe/stripe-go/v82 v82.0.0 ``` ```java package com.stripe.sample; import java.nio.file.Paths; import static spark.Spark.get; import static spark.Spark.post; import static spark.Spark.staticFiles; import static spark.Spark.port; import com.google.gson.Gson; import com.google.gson.annotations.SerializedName; import com.stripe.Stripe; import com.stripe.model.PaymentIntent; import com.stripe.param.PaymentIntentCreateParams; import com.stripe.model.terminal.ConnectionToken; import com.stripe.model.terminal.Reader; import com.stripe.param.terminal.ReaderProcessPaymentIntentParams; import com.stripe.param.terminal.ReaderCreateParams; import com.stripe.param.terminal.ConnectionTokenCreateParams; import com.stripe.model.terminal.Location; import com.stripe.param.terminal.LocationCreateParams; import com.stripe.exception.InvalidRequestException; import com.stripe.exception.StripeException; import java.util.Collections; import java.util.Map; import java.util.HashMap; public class Server { private static Gson gson = new Gson(); static class PaymentIntentParams { private String payment_intent_id; private long amount; public String getPaymentIntentId() { return payment_intent_id; } public long getAmount() { return amount; } } static class ReaderParams { private String reader_id; private String location_id; public String getReaderId() { return reader_id; } public String getLocationId() { return location_id; } } static class ProcessPaymentParams { private String reader_id; private String payment_intent_id; public String getReaderId() { return reader_id; } public String getPaymentIntentId() { return payment_intent_id; } } public static void main(String[] args) { port(4242); staticFiles.externalLocation(Paths.get("public").toAbsolutePath().toString()); Stripe.apiKey = "<>"; post("/create_location", (request, response) -> { LocationCreateParams.Address address = LocationCreateParams.Address.builder() .setLine1("{{TERMINAL_LOCATION_LINE1}}") .setLine2("{{TERMINAL_LOCATION_LINE2}}") .setCity("{{TERMINAL_LOCATION_CITY}}") .setState("{{TERMINAL_LOCATION_STATE}}") .setCountry("{{TERMINAL_LOCATION_COUNTRY}}") .setPostalCode("{{TERMINAL_LOCATION_POSTAL}}") .build(); LocationCreateParams params = LocationCreateParams.builder() .setDisplayName("{{TERMINAL_LOCATION_NAME}}") .setAddress(address) .build(); Location location = Location.create(params); return location.toJson(); }); // The ConnectionToken's secret lets you connect to any Stripe Terminal reader // and take payments with your Stripe account. // Be sure to authenticate the endpoint for creating connection tokens. post("/connection_token", (request, response) -> { response.type("application/json"); ConnectionTokenCreateParams params = ConnectionTokenCreateParams.builder() .build(); ConnectionToken connectionToken = ConnectionToken.create(params); Map map = new HashMap(); map.put("secret", connectionToken.getSecret()); return gson.toJson(map); }); post("/register_reader", (request, response) -> { ReaderParams postBody = gson.fromJson(request.body(), ReaderParams.class); ReaderCreateParams params = ReaderCreateParams.builder() .setRegistrationCode("simulated-wpe") .setLocation(postBody.getLocationId()) .build(); Reader reader = Reader.create(params); return reader.toJson(); }); post("/create_payment_intent", (request, response) -> { response.type("application/json"); PaymentIntentParams postBody = gson.fromJson(request.body(), PaymentIntentParams.class); // For Terminal payments, the 'payment_method_types' parameter must include // 'card_present'. // To automatically capture funds when a charge is authorized, // set `capture_method` to `automatic`. PaymentIntentCreateParams createParams = PaymentIntentCreateParams.builder() .setCurrency("{{TERMINAL_CURRENCY}}") .setAmount(postBody.getAmount()) .setCaptureMethod(PaymentIntentCreateParams.CaptureMethod.MANUAL) {{TERMINAL_PAYMENT_METHODS}} .build(); // Create a PaymentIntent with the order amount and currency PaymentIntent intent = PaymentIntent.create(createParams); return intent.toJson(); }); post("/process_payment", (request, response) -> { ProcessPaymentParams postBody = gson.fromJson(request.body(), ProcessPaymentParams.class); ReaderProcessPaymentIntentParams params = ReaderProcessPaymentIntentParams.builder() .setPaymentIntent(postBody.getPaymentIntentId()) .build(); Reader reader = Reader.retrieve(postBody.getReaderId()); int attempt = 0; int tries = 3; while (true) { attempt++; try { reader = reader.processPaymentIntent(params); return reader.toJson(); } catch (InvalidRequestException e) { switch (e.getCode()) { case "terminal_reader_timeout": // Temporary networking blip, automatically retry a few times. if (attempt == tries) { return e.getStripeError().toJson(); } break; case "terminal_reader_offline": // Reader is offline and won't respond to API requests. Make sure the reader is // powered on and connected to the internet before retrying. return e.getStripeError().toJson(); case "terminal_reader_busy": // Reader is currently busy processing another request, installing updates or // changing settings. Remember to disable the pay button in your point-of-sale // application while waiting for a reader to respond to an API request. return e.getStripeError().toJson(); case "intent_invalid_state": // Check PaymentIntent status because it's not ready to be processed. It might // have been already successfully processed or canceled. PaymentIntent paymentIntent = PaymentIntent.retrieve(postBody.getPaymentIntentId()); Map errorResponse = Collections.singletonMap("error", "PaymentIntent is already in " + paymentIntent.getStatus() + " state."); return new Gson().toJson(errorResponse); default: return e.getStripeError().toJson(); } } } }); post("/simulate_payment", (request, response) -> { ReaderParams postBody = gson.fromJson(request.body(), ReaderParams.class); Reader reader = Reader.retrieve(postBody.getReaderId()); reader = reader.getTestHelpers().presentPaymentMethod(); return reader.toJson(); }); post("/capture_payment_intent", (request, response) -> { response.type("application/json"); PaymentIntentParams postBody = gson.fromJson(request.body(), PaymentIntentParams.class); PaymentIntent intent = PaymentIntent.retrieve(postBody.getPaymentIntentId()); intent = intent.capture(); return intent.toJson(); }); } public static Location createLocation() throws StripeException{ LocationCreateParams.Address address = LocationCreateParams.Address.builder() .setLine1("{{TERMINAL_LOCATION_LINE1}}") .setCity("{{TERMINAL_LOCATION_CITY}}") .setState("{{TERMINAL_LOCATION_STATE}}") .setCountry("{{TERMINAL_LOCATION_COUNTRY}}") .setPostalCode("{{TERMINAL_LOCATION_POSTAL}}") .build(); LocationCreateParams params = LocationCreateParams.builder() .setDisplayName("{{TERMINAL_LOCATION_NAME}}") .setAddress(address) .build(); Location location = Location.create(params); return location; } } ``` ```xml 4.0.0 com.stripe.sample stripe-payment 1.0.0-SNAPSHOT org.slf4j slf4j-simple 2.0.3 com.sparkjava spark-core 2.9.4 com.google.code.gson gson 2.9.1 org.projectlombok lombok 1.18.20 provided com.stripe stripe-java 29.0.0 sample org.apache.maven.plugins maven-compiler-plugin 3.10.1 1.8 1.8 maven-assembly-plugin package single jar-with-dependencies Server ``` ```javascript export const fetchConnectionToken = async () => { const response = await fetch('http://localhost:4242/connection_token', { method: 'POST', headers: { 'Content-Type': 'application/json', }, }); const data = await response.json(); if (!data) { throw Error('No data in response from ConnectionToken endpoint'); } if (!data.secret) { throw Error('Missing `secret` in ConnectionToken JSON response'); } return data.secret; }; export const fetchPaymentIntent = async () => { const parameters = { amount: 1000, }; const response = await fetch('http://localhost:4242/create_payment_intent', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(parameters), }); const data = await response.json(); if (!data) { throw Error('No data in response from PaymentIntent endpoint'); } if (!data.client_secret) { throw Error('Missing `client_secret` in ConnectionToken JSON response'); } return data.client_secret; }; export const capturePaymentIntent = async () => { const parameters = { id: 'paymentIntentId', }; const response = await fetch('http://localhost:4242/capture_payment_intent', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(parameters), }); if (response.status >= 200 && response.status < 300) { return true; } else { return false; } }; ``` ```javascript import React, {useEffect, useState} from 'react'; import { TouchableOpacity, Text, PermissionsAndroid, Platform, Alert, StyleSheet, ActivityIndicator, View, } from 'react-native'; import { useStripeTerminal, } from '@stripe/stripe-terminal-react-native'; import { fetchPaymentIntent, capturePaymentIntent, } from './apiClient'; export default function App() { const { discoverReaders, retrievePaymentIntent, connectInternetReader, connectBluetoothReader, collectPaymentMethod, confirmPaymentIntent, setSimulatedCard, } = useStripeTerminal({ onUpdateDiscoveredReaders: async (readers) => { const selectedReader = readers[0]; const {reader, error} = await connectInternetReader({ readerId: selectedReader.id, // for simulated mode you can provide the simulated reader’s mock locationId locationId: selectedReader.locationId, }); const {reader, error} = await connectBluetoothReader({ readerId: selectedReader.id, }); if (error) { console.log('connectBluetoothReader error', error); } else { console.log('Reader connected successfully', reader); } }, }); const [permissionsGranted, setPermissionsGranted] = useState(false); useEffect(() => { async function init() { try { const granted = await PermissionsAndroid.request( 'android.permission.ACCESS_FINE_LOCATION', { title: 'Location Permission', message: 'Stripe Terminal needs access to your location', buttonPositive: 'Accept', }, ); if (granted === PermissionsAndroid.RESULTS.GRANTED) { console.log('You can use the Location'); setPermissionsGranted(true); } else { Alert.alert( 'Location services are required in order to connect to a reader.', ); } } catch { Alert.alert( 'Location services are required in order to connect to a reader.', ); } } if (Platform.OS === 'android') { init(); } else { setPermissionsGranted(true); } }, []); const handleDiscoverReaders = async () => { // List of discovered readers will be available within useStripeTerminal hook const {error} = await discoverReaders({ discoveryMethod: 'internet', simulated: true, }); const {error} = await discoverReaders({ discoveryMethod: 'bluetoothScan', simulated: true, }); if (error) { console.log( 'Discover readers error: ', `${error.code}, ${error.message}`, ); } else { console.log('discoverReaders succeeded'); } }; const collectPayment = async () => { const clientSecret = await fetchPaymentIntent(); await setSimulatedCard("4242424242424242"); if (!clientSecret) { console.log('createPaymentIntent failed'); return; } const {paymentIntent, error} = await retrievePaymentIntent(clientSecret); if (error) { console.log(`Couldn't retrieve payment intent: ${error.message}`); } else if (paymentIntent) { const {paymentIntent: collectedPaymentIntent, error: collectError} = await collectPaymentMethod(paymentIntent.id); if (collectError) { console.log(`collectPaymentMethod failed: ${collectError.message}`); } else if (collectedPaymentIntent) { console.log('collectPaymentMethod succeeded'); processPayment(collectedPaymentIntent); } } }; const processPayment = async (paymentIntent) => { const {paymentIntent: processPaymentPaymentIntent, error} = await confirmPaymentIntent(paymentIntent); if (error) { console.log(`confirmPaymentIntent failed: ${error.message}`); } else if (processPaymentPaymentIntent) { console.log('confirmPaymentIntent succeeded'); const result = await capturePaymentIntent(); if (!result) { console.log('capture failed'); } else { console.log('capture succeeded'); } } }; return ( <> {permissionsGranted ? ( Discover Readers Collect payment ) : ( )} ); } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', // Center vertically alignItems: 'center', // Center horizontally }, }); ``` ```javascript import React from 'react'; import { StripeTerminalProvider, } from '@stripe/stripe-terminal-react-native'; import { fetchConnectionToken, } from './apiClient'; import App from './App'; export default function Root() { return ( <> ); } ``` ```html Accept in-person payments
Simulate reader pairing

Simulate a transaction
Enter an amount
Logs
``` ```css /* Variables */ * { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: none; -webkit-box-sizing: border-box; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif; line-height: 1.4em; } html, body { overflow: hidden; height: 100%; } .pad { padding-right: 35px; padding-left: 35px; } hr { margin-right: 20px; margin-left: 20px; } #logs>hr { margin-right: 0px; margin-left: 0px; } svg { height: -webkit-fill-available; float: right; } .margin { margin-top: 15px; } .title { padding-right: 35px; padding-left: 35px; margin-top: 20px; margin-bottom: 5px; font-size: 20px; font-weight: 600; } .text { font-size: 15px; font-weight: 500; color: #525f7f; } .log-col { overflow-y: scroll; } #logs { padding-right: 35px; padding-left: 35px; margin-top: 20px; margin-bottom: 5px; color: black; } .offset { background: #f7fafc; } .green { color: #09825d; } .log-title { font-size: 16px; font-weight: 500; padding-left: 15px; margin-bottom: 10px; } .log { padding-left: 15px; } pre { color: #585050 !important; } button { color: black; padding: 16px; border: 0; background: white; -webkit-box-shadow: none; box-shadow: none; outline: none; cursor: pointer; -webkit-transition: all 0.15s ease; -o-transition: all 0.15s ease; transition: all 0.15s ease; width: 100%; height: 54px; border-radius: 6px !important; font-size: 16px; font-weight: 500; text-align: left; text-transform: capitalize; box-shadow: 0px 7px 14px rgba(60, 66, 87, 0.12), 0px 3px 6px rgba(0, 0, 0, 0.08); } .input-icon { position: relative; border-radius: 4px; } .input-icon>i { position: absolute; border-radius: 4px; display: block; transform: translate(0, -50%); top: 50%; pointer-events: none; width: 25px; text-align: center; font-style: normal; } .input-icon>input { border-radius: 4px; border: 1px solid #E3E8EE; width: 100%; height: 40px; color: black; font-size: 16px; font-weight: normal; padding-left: 25px; padding-right: 0; } ``` ```javascript var terminal = StripeTerminal.create({ onFetchConnectionToken: fetchConnectionToken, onUnexpectedReaderDisconnect: unexpectedDisconnect, }); function unexpectedDisconnect() { // In this function, your app should notify the user that the reader disconnected. // You can also include a way to attempt to reconnect to a reader. console.log("Disconnected from reader") } function fetchConnectionToken() { // Do not cache or hardcode the ConnectionToken. The SDK manages the ConnectionToken's lifecycle. return fetch('/connection_token', { method: "POST" }) .then(function(response) { return response.json(); }) .then(function(data) { return data.secret; }); } // Handler for a "Discover readers" button function discoverReaderHandler() { var config = {simulated: true}; terminal.discoverReaders(config).then(function(discoverResult) { if (discoverResult.error) { console.log('Failed to discover: ', discoverResult.error); } else if (discoverResult.discoveredReaders.length === 0) { console.log('No available readers.'); } else { discoveredReaders = discoverResult.discoveredReaders; log('terminal.discoverReaders', discoveredReaders); } }); } // Handler for a "Connect Reader" button function connectReaderHandler(discoveredReaders) { // Just select the first reader here. var selectedReader = discoveredReaders[0]; terminal.connectReader(selectedReader).then(function(connectResult) { if (connectResult.error) { console.log('Failed to connect: ', connectResult.error); } else { console.log('Connected to reader: ', connectResult.reader.label); log('terminal.connectReader', connectResult) } }); } function fetchPaymentIntentClientSecret(amount) { const bodyContent = JSON.stringify({ amount: amount }); return fetch('/create_payment_intent', { method: "POST", headers: { 'Content-Type': 'application/json' }, body: bodyContent }) .then(function(response) { return response.json(); }) .then(function(data) { return data.client_secret; }); } function collectPayment(amount) { fetchPaymentIntentClientSecret(amount).then(function(client_secret) { terminal.setSimulatorConfiguration({testCardNumber: '4242424242424242'}); terminal.collectPaymentMethod(client_secret).then(function(result) { if (result.error) { // Placeholder for handling result.error } else { log('terminal.collectPaymentMethod', result.paymentIntent); terminal.processPayment(result.paymentIntent).then(function(result) { if (result.error) { console.log(result.error) } else if (result.paymentIntent) { paymentIntentId = result.paymentIntent.id; log('terminal.processPayment', result.paymentIntent); } }); } }); }); } function capture(paymentIntentId) { return fetch('/capture_payment_intent', { method: "POST", headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({"payment_intent_id": paymentIntentId}) }) .then(function(response) { return response.json(); }) .then(function(data) { log('server.capture', data); }); } var discoveredReaders; var paymentIntentId; const discoverButton = document.getElementById('discover-button'); discoverButton.addEventListener('click', async (event) => { discoverReaderHandler(); }); const connectButton = document.getElementById('connect-button'); connectButton.addEventListener('click', async (event) => { connectReaderHandler(discoveredReaders); }); const collectButton = document.getElementById('collect-button'); collectButton.addEventListener('click', async (event) => { amount = document.getElementById("amount-input").value collectPayment(amount); }); const captureButton = document.getElementById('capture-button'); captureButton.addEventListener('click', async (event) => { capture(paymentIntentId); }); function log(method, message){ var logs = document.getElementById("logs"); var title = document.createElement("div"); var log = document.createElement("div"); var lineCol = document.createElement("div"); var logCol = document.createElement("div"); title.classList.add('row'); title.classList.add('log-title'); title.textContent = method; log.classList.add('row'); log.classList.add('log'); var hr = document.createElement("hr"); var pre = document.createElement("pre"); var code = document.createElement("code"); code.textContent = formatJson(JSON.stringify(message, undefined, 2)); pre.append(code); log.append(pre); logs.prepend(hr); logs.prepend(log); logs.prepend(title); } function stringLengthOfInt(number) { return number.toString().length; } function padSpaces(lineNumber, fixedWidth) { // Always indent by 2 and then maybe more, based on the width of the line // number. return " ".repeat(2 + fixedWidth - stringLengthOfInt(lineNumber)); } function formatJson(message){ var lines = message.split('\n'); var json = ""; var lineNumberFixedWidth = stringLengthOfInt(lines.length); for(var i = 1; i <= lines.length; i += 1){ line = i + padSpaces(i, lineNumberFixedWidth) + lines[i-1]; json = json + line + '\n'; } return json } ``` ```javascript const terminal = StripeTerminal.create({ onFetchConnectionToken: fetchConnectionToken, onUnexpectedReaderDisconnect: unexpectedDisconnect, }); function unexpectedDisconnect() { // In this function, your app should notify the user that the reader disconnected. // You can also include a way to attempt to reconnect to a reader. console.log("Disconnected from reader") } async function fetchConnectionToken() { // Do not cache or hardcode the ConnectionToken. The SDK manages the ConnectionToken's lifecycle. const response = await fetch('/connection_token', { method: "POST" }); const data = await response.json(); return data.secret; } // Handler for a "Discover readers" button function discoverReaderHandler() { const config = {simulated: true}; const discoverResult = await terminal.discoverReaders(config); if (discoverResult.error) { console.log('Failed to discover: ', discoverResult.error); } else if (discoverResult.discoveredReaders.length === 0) { console.log('No available readers.'); } else { discoveredReaders = discoverResult.discoveredReaders; log('terminal.discoverReaders', discoveredReaders); } } // Handler for a "Connect Reader" button function connectReaderHandler(discoveredReaders) { // Just select the first reader here. const selectedReader = discoveredReaders[0]; const connectResult = await terminal.connectReader(selectedReader); if (connectResult.error) { console.log('Failed to connect: ', connectResult.error); } else { console.log('Connected to reader: ', connectResult.reader.label); log('terminal.connectReader', connectResult) } } async function fetchPaymentIntentClientSecret(amount) { const bodyContent = JSON.stringify({ amount: amount }); const response = await fetch('/create_payment_intent', { method: "POST", headers: { 'Content-Type': 'application/json' }, body: bodyContent }): const data = await response.json(); return data.client_secret; } async function collectPayment(amount) { const client_secret = await fetchPaymentClientSecret(amount); terminal.setSimulatorConfiguration({testCardNumber: '4242424242424242'}); const result = await terminal.collectPaymentMethod(client_secret) if (result.error) { // Placeholder for handling result.error } else { log('terminal.collectPaymentMethod', result.paymentIntent); const result = await terminal.processPayment(result.paymentIntent) { if (result.error) { // Placeholder for handling result.error } else if (result.paymentIntent) { paymentIntentId = result.paymentIntent.id; log('processPayment', result.paymentIntent); } }; } } async function capture(paymentIntentId) { const result = await fetch('/capture_payment_intent', { method: "POST", headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({"payment_intent_id": paymentIntentId}) }) const data = result.json(); log('server.capture', data); } var discoveredReaders; var paymentIntentId; const discoverButton = document.getElementById('discover-button'); discoverButton.addEventListener('click', async (event) => { discoverReaderHandler(); }); const connectButton = document.getElementById('connect-button'); connectButton.addEventListener('click', async (event) => { connectReaderHandler(discoveredReaders); }); const collectButton = document.getElementById('collect-button'); collectButton.addEventListener('click', async (event) => { amount = document.getElementById("amount-input").value collectPayment(amount); }); const captureButton = document.getElementById('capture-button'); captureButton.addEventListener('click', async (event) => { capture(paymentIntentId); }); function log(method, message){ var logs = document.getElementById("logs"); var title = document.createElement("div"); var log = document.createElement("div"); var lineCol = document.createElement("div"); var logCol = document.createElement("div"); title.classList.add('row'); title.classList.add('log-title'); title.textContent = method; log.classList.add('row'); log.classList.add('log'); var hr = document.createElement("hr"); var pre = document.createElement("pre"); var code = document.createElement("code"); code.textContent = formatJson(JSON.stringify(message, undefined, 2)); pre.append(code); log.append(pre); logs.prepend(hr); logs.prepend(log); logs.prepend(title); } function formatJson(message){ var lines = message.split('\n'); var space = " ".repeat(2); var json = ""; for(var i = 1; i <= lines.length; i += 1){ line = i + space + lines[i-1]; json = json + line + '\n'; } return json } ``` ```html Accept in-person payments
Simulate reader pairing

Simulate a transaction
Enter an amount
Logs
``` ```css /* Variables */ * { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: none; -webkit-box-sizing: border-box; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif; line-height: 1.4em; } html, body { overflow: hidden; height: 100%; } .pad { padding-right: 35px; padding-left: 35px; } hr { margin-right: 20px; margin-left: 20px; } #logs>hr { margin-right: 0px; margin-left: 0px; } svg { height: -webkit-fill-available; float: right; } .margin { margin-top: 15px; } .title { padding-right: 35px; padding-left: 35px; margin-top: 20px; margin-bottom: 5px; font-size: 20px; font-weight: 600; } .text { font-size: 15px; font-weight: 500; color: #525f7f; } .log-col { overflow-y: scroll; } #logs { padding-right: 35px; padding-left: 35px; margin-top: 20px; margin-bottom: 5px; color: black; } .offset { background: #f7fafc; } .green { color: #09825d; } .log-title { font-size: 16px; font-weight: 500; padding-left: 15px; margin-bottom: 10px; } .log { padding-left: 15px; } pre { color: #585050 !important; } button { color: black; padding: 16px; border: 0; background: white; -webkit-box-shadow: none; box-shadow: none; outline: none; cursor: pointer; -webkit-transition: all 0.15s ease; -o-transition: all 0.15s ease; transition: all 0.15s ease; width: 100%; height: 54px; border-radius: 6px !important; font-size: 16px; font-weight: 500; text-align: left; text-transform: capitalize; box-shadow: 0px 7px 14px rgba(60, 66, 87, 0.12), 0px 3px 6px rgba(0, 0, 0, 0.08); } .input-icon { position: relative; border-radius: 4px; } .input-icon>i { position: absolute; border-radius: 4px; display: block; transform: translate(0, -50%); top: 50%; pointer-events: none; width: 25px; text-align: center; font-style: normal; } .input-icon>input { border-radius: 4px; border: 1px solid #E3E8EE; width: 100%; height: 40px; color: black; font-size: 16px; font-weight: normal; padding-left: 25px; padding-right: 0; } ``` ```html Accept in-person payments
Create a simulated reader

Simulate a transaction
Enter an amount
Logs
``` ```css /* Variables */ * { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: none; -webkit-box-sizing: border-box; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif; line-height: 1.4em; } html, body { overflow: hidden; height: 100%; } .pad { padding-right: 35px; padding-left: 35px; } hr { margin-right: 20px; margin-left: 20px; } #logs>hr { margin-right: 0px; margin-left: 0px; } .right-arrow { height: -webkit-fill-available; float: right; } .spinner { width:30px; height:30px; border:3px solid #666; border-top:3px solid white; border-radius:50%; position: absolute; right: 50px; top: 12px; display: none; -webkit-transition-property: -webkit-transform; -webkit-transition-duration: 1.2s; -webkit-animation-name: rotate; -webkit-animation-iteration-count: infinite; -webkit-animation-timing-function: linear; -moz-transition-property: -moz-transform; -moz-animation-name: rotate; -moz-animation-duration: 1.2s; -moz-animation-iteration-count: infinite; -moz-animation-timing-function: linear; transition-property: transform; transition-duration: 1.2s; animation-name: rotate; animation-duration: 1.2s; animation-iteration-count: infinite; animation-timing-function: linear; } .loading .spinner { display: block; } @-webkit-keyframes rotate { from {-webkit-transform: rotate(0deg);} to {-webkit-transform: rotate(360deg);} } @-moz-keyframes rotate { from {-moz-transform: rotate(0deg);} to {-moz-transform: rotate(360deg);} } @keyframes rotate { from {transform: rotate(0deg);} to {transform: rotate(360deg);} } .margin { margin-top: 15px; } .title { padding-right: 35px; padding-left: 35px; margin-top: 20px; margin-bottom: 5px; font-size: 20px; font-weight: 600; } .text { font-size: 15px; font-weight: 500; color: #525f7f; } .log-col { overflow-y: scroll; } #logs { padding-right: 35px; padding-left: 35px; margin-top: 20px; margin-bottom: 5px; color: black; } .offset { background: #f7fafc; } .green { color: #09825d; } .log-title { font-size: 16px; font-weight: 500; padding-left: 15px; margin-bottom: 10px; } .log { padding-left: 15px; } pre { color: #585050 !important; } button { color: black; padding: 16px; border: 0; background: white; -webkit-box-shadow: none; box-shadow: none; outline: none; cursor: pointer; -webkit-transition: all 0.15s ease; -o-transition: all 0.15s ease; transition: all 0.15s ease; width: 100%; height: 54px; border-radius: 6px !important; font-size: 16px; font-weight: 500; text-align: left; text-transform: capitalize; box-shadow: 0px 7px 14px rgba(60, 66, 87, 0.12), 0px 3px 6px rgba(0, 0, 0, 0.08); position: relative; } .input-icon { position: relative; border-radius: 4px; } .input-icon>i { position: absolute; border-radius: 4px; display: block; transform: translate(0, -50%); top: 50%; pointer-events: none; width: 25px; text-align: center; font-style: normal; } .input-icon>input { border-radius: 4px; border: 1px solid #E3E8EE; width: 100%; height: 40px; color: black; font-size: 16px; font-weight: normal; padding-left: 25px; padding-right: 0; } ``` ```javascript function createLocation() { return fetch("/create_location", { method: "POST" }).then((response) => { return response.json(); }); } function createReader() { return fetch("/register_reader", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location_id: locationId }), }).then((response) => { return response.json(); }); } function createPaymentIntent(amount) { return fetch("/create_payment_intent", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ amount: amount }), }).then((response) => { return response.json(); }); } function processPayment() { return fetch("/process_payment", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ reader_id: readerId, payment_intent_id: paymentIntentId, }), }).then((response) => { return response.json(); }); } function simulatePayment() { return fetch("/simulate_payment", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ reader_id: readerId, }), }).then((response) => { return response.json(); }); } function capture(paymentIntentId) { return fetch("/capture_payment_intent", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ payment_intent_id: paymentIntentId }), }).then((response) => { return response.json(); }); } var locationId; var readerId; var paymentIntentId; const createLocationButton = document.getElementById("create-location-button"); createLocationButton.addEventListener("click", async (event) => { createLocationButton.className = "loading"; createLocationButton.disabled = true; createLocation().then((location) => { createLocationButton.className = ""; createLocationButton.disabled = false; log("POST /v1/terminal/locations", location); locationId = location["id"]; }); }); const createReaderButton = document.getElementById("create-reader-button"); createReaderButton.addEventListener("click", async (event) => { createReaderButton.className = "loading"; createReaderButton.disabled = true; createReader().then((reader) => { createReaderButton.className = ""; createReaderButton.disabled = false; log("POST /v1/terminal/readers", reader); readerId = reader["id"]; }); }); const createPaymentButton = document.getElementById("create-payment-button"); createPaymentButton.addEventListener("click", async (event) => { createPaymentButton.className = "loading"; createPaymentButton.disabled = true; amount = document.getElementById("amount-input").value; createPaymentIntent(amount).then((paymentIntent) => { createPaymentButton.className = ""; createPaymentButton.disabled = false; log("POST /v1/payment_intents", paymentIntent); paymentIntentId = paymentIntent["id"]; }); }); const processPaymentButton = document.getElementById("process-payment-button"); processPaymentButton.addEventListener("click", async (event) => { processPaymentButton.className = "loading"; processPaymentButton.disabled = true; processPayment().then((reader) => { processPaymentButton.className = ""; processPaymentButton.disabled = false; log( "POST /v1/terminal/readers/" + readerId + "/process_payment_intent", reader ); }); }); const simulatePaymentButton = document.getElementById( "simulate-payment-button" ); simulatePaymentButton.addEventListener("click", async (event) => { simulatePaymentButton.className = "loading"; simulatePaymentButton.disabled = true; simulatePayment().then((reader) => { simulatePaymentButton.className = ""; simulatePaymentButton.disabled = false; log( "POST /v1/test_helpers/terminal/readers/" + readerId + "/present_payment_method", reader ); }); }); const captureButton = document.getElementById("capture-button"); captureButton.addEventListener("click", async (event) => { captureButton.className = "loading"; captureButton.disabled = true; capture(paymentIntentId).then((paymentIntent) => { captureButton.className = ""; captureButton.disabled = false; log( "POST /v1/payment_intents/" + paymentIntentId + "/capture", paymentIntent ); }); }); function log(method, message) { var logs = document.getElementById("logs"); var title = document.createElement("div"); var log = document.createElement("div"); title.classList.add("row"); title.classList.add("log-title"); title.textContent = method; log.classList.add("row"); log.classList.add("log"); var hr = document.createElement("hr"); var pre = document.createElement("pre"); var code = document.createElement("code"); code.textContent = formatJson(JSON.stringify(message, undefined, 2)); pre.append(code); log.append(pre); logs.prepend(hr); logs.prepend(log); logs.prepend(title); } function stringLengthOfInt(number) { return number.toString().length; } function padSpaces(lineNumber, fixedWidth) { // Always indent by 2 and then maybe more, based on the width of the line // number. return " ".repeat(2 + fixedWidth - stringLengthOfInt(lineNumber)); } function formatJson(message) { var lines = message.split("\n"); var json = ""; var lineNumberFixedWidth = stringLengthOfInt(lines.length); for (var i = 1; i <= lines.length; i += 1) { line = i + padSpaces(i, lineNumberFixedWidth) + lines[i - 1]; json = json + line + "\n"; } return json; } ``` ```swift import UIKit import StripeTerminal class ViewController: UIViewController { var discoverCancelable: Cancelable? var collectCancelable: Cancelable? var nextActionButton = UIButton(type: .system) var readerMessageLabel = UILabel() override func viewDidLoad() { super.viewDidLoad() setUpInterface() // Move this to your App Delegate, in didFinishLaunchingWithOptions. Terminal.setTokenProvider(APIClient.shared) } @objc func discoverReaders() throws { let config = try InternetDiscoveryConfigurationBuilder().setSimulated(true).build() self.discoverCancelable = Terminal.shared.discoverReaders(config, delegate: self) { error in let config = try BluetoothScanDiscoveryConfigurationBuilder().setSimulated(true).build() self.discoverCancelable = Terminal.shared.discoverReaders(config, delegate: self) { error in if let error = error { print("discoverReaders failed: \(error)") } else { print("discoverReaders succeeded") self.nextActionButton.setTitle("Make a Payment", for: .normal) self.nextActionButton.removeTarget(self, action: #selector(self.discoverReaders), for: .touchUpInside) self.nextActionButton.addTarget(self, action: #selector(self.collectPayment), for: .touchUpInside) } } } @objc func collectPayment() throws { let params = try PaymentIntentParametersBuilder(amount: 1000, currency: "{{TERMINAL_CURRENCY}}") .setPaymentMethodTypes([{{TERMINAL_PAYMENT_METHODS}}]) .build() Terminal.shared.createPaymentIntent(params) { createResult, createError in if let error = createError { print("createPaymentIntent failed: \(error)") } else if let paymentIntent = createResult { print("createPaymentIntent succeeded") Terminal.shared.collectPaymentMethod(paymentIntent) { collectResult, collectError in if let error = collectError { print("collectPaymentMethod failed: \(error)") } else if let paymentIntent = collectResult { print("collectPaymentMethod succeeded") self.confirmPaymentIntent(paymentIntent) } } } } } private func confirmPaymentIntent(_ paymentIntent: PaymentIntent) { Terminal.shared.confirmPaymentIntent(paymentIntent) { confirmResult, confirmError in if let error = confirmError { print("confirmPaymentIntent failed: \(error)") } else if let confirmedPaymentIntent = confirmResult { print("confirmPaymentIntent succeeded") if let stripeId = confirmedPaymentIntent.stripeId { // Notify your backend to capture the PaymentIntent. // PaymentIntents processed with Stripe Terminal must be captured // within 24 hours of processing the payment. APIClient.shared.capturePaymentIntent(stripeId) { captureError in if let error = captureError { print("capture failed: \(error)") } else { print("capture succeeded") self.readerMessageLabel.text = "Payment captured" } } } else { print("Payment collected offline") } } } } func setUpInterface() { readerMessageLabel.textAlignment = .center readerMessageLabel.numberOfLines = 0 nextActionButton.setTitle("Connect to a reader", for: .normal) nextActionButton.addTarget(self, action: #selector(discoverReaders), for: .touchUpInside) let stackView = UIStackView(arrangedSubviews: [nextActionButton, readerMessageLabel]) stackView.axis = .vertical stackView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(stackView) NSLayoutConstraint.activate([ stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor), stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor), stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor), stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor), ]) } } extension ViewController: DiscoveryDelegate { func terminal(_ terminal: Terminal, didUpdateDiscoveredReaders readers: [Reader]) { self.readerMessageLabel.text = "\(readers.count) readers found" // Select the first reader the SDK discovers. In your app, // you should display the available readers to your user, then // connect to the reader they've selected. guard let selectedReader = readers.first else { return } // Only connect if we aren't currently connected. guard terminal.connectionStatus == .notConnected || terminal.connectionStatus == .discovering else { return } let connectionConfig: InternetConnectionConfiguration do { connectionConfig = try InternetConnectionConfigurationBuilder(delegate: self).build() } catch { // Handle the error building the connection configuration return } let connectionConfig: BluetoothConnectionConfiguration do { connectionConfig = try BluetoothConnectionConfigurationBuilder( delegate: self, // When connecting to a physical reader, your integration should specify either the // same location as the last connection (selectedReader.locationId) or a new location // of your user's choosing. // // Since the simulated reader is not associated with a real location, we recommend // specifying its existing mock location. locationId: selectedReader.locationId! ).build() } catch { // Handle the error building the connection configuration return } Terminal.shared.connectReader(selectedReader, connectionConfig: connectionConfig) { reader, error in if let reader = reader { print("Successfully connected to reader: \(reader)") } else if let error = error { print("connectReader failed: \(error)") } } } } extension ViewController: MobileReaderDelegate { func reader(_ reader: Reader, didDisconnect reason: DisconnectReason) { // Handle reader disconnect } func reader(_ reader: Reader, didRequestReaderInput inputOptions: ReaderInputOptions = []) { readerMessageLabel.text = Terminal.stringFromReaderInputOptions(inputOptions) } func reader(_ reader: Reader, didRequestReaderDisplayMessage displayMessage: ReaderDisplayMessage) { readerMessageLabel.text = Terminal.stringFromReaderDisplayMessage(displayMessage) } func reader(_ reader: Reader, didStartInstallingUpdate update: ReaderSoftwareUpdate, cancelable: Cancelable?) { // Show UI communicating that a required update has started installing } func reader(_ reader: Reader, didReportReaderSoftwareUpdateProgress progress: Float) { // Update the progress of the install } func reader(_ reader: Reader, didFinishInstallingUpdate update: ReaderSoftwareUpdate?, error: Error?) { // Report success or failure of the update } func reader(_ reader: Reader, didReportAvailableUpdate update: ReaderSoftwareUpdate) { // Show UI communicating that an update is available } } extension ViewController: InternetReaderDelegate { func reader(_ reader: Reader, didDisconnect reason: DisconnectReason) { print("Disconnected from reader: \(reader)") } } ``` ```swift import Foundation import StripeTerminal class APIClient: ConnectionTokenProvider { // For simplicity, this example class is a singleton static let shared = APIClient() static let backendUrl = URL(string: "http://localhost:4242")! func fetchConnectionToken(_ completion: @escaping ConnectionTokenCompletionBlock) { let config = URLSessionConfiguration.default let session = URLSession(configuration: config) let url = URL(string: "/connection_token", relativeTo: APIClient.backendUrl)! var request = URLRequest(url: url) request.httpMethod = "POST" let task = session.dataTask(with: request) { (data, response, error) in if let data = data { do { // Warning: casting using 'as? [String: String]' looks simpler, but isn't safe: let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] if let secret = json?["secret"] as? String { completion(secret, nil) } else { let error = NSError(domain: "com.stripe-terminal-ios.example", code: 2000, userInfo: [NSLocalizedDescriptionKey: "Missing 'secret' in ConnectionToken JSON response"]) completion(nil, error) } } catch { completion(nil, error) } } else { let error = NSError(domain: "com.stripe-terminal-ios.example", code: 1000, userInfo: [NSLocalizedDescriptionKey: "No data in response from ConnectionToken endpoint"]) completion(nil, error) } } task.resume() } func capturePaymentIntent(_ paymentIntentId: String, completion: @escaping ErrorCompletionBlock) { let config = URLSessionConfiguration.default let session = URLSession(configuration: config, delegate: nil, delegateQueue: OperationQueue.main) let url = URL(string: "/capture_payment_intent", relativeTo: APIClient.backendUrl)! let parameters = "{\"payment_intent_id\": \"\(paymentIntentId)\"}" var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type") request.httpBody = parameters.data(using: .utf8) let task = session.dataTask(with: request) {(data, response, error) in if let response = response as? HTTPURLResponse, let data = data { switch response.statusCode { case 200..<300: completion(nil) case 402: let description = String(data: data, encoding: .utf8) ?? "Failed to capture payment intent" completion(NSError(domain: "com.stripe-terminal-ios.example", code: 2, userInfo: [NSLocalizedDescriptionKey: description])) default: completion(error ?? NSError(domain: "com.stripe-terminal-ios.example", code: 0, userInfo: [NSLocalizedDescriptionKey: "Other networking error encountered."])) } } else { completion(error) } } task.resume() } } ``` ```objectivec \#import #import "ViewController.h" #import "APIClient.h" @interface ViewController () @interface ViewController () @property (nonatomic, nullable, strong) SCPCancelable *discoverCancelable; @property (nonatomic, nullable, strong) SCPCancelable *collectCancelable; @property (nonatomic, strong) UIButton *nextActionButton; @property (nonatomic, strong) UILabel *readerMessageLabel; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; [self setUpInterface]; // Move this to your App Delegate, in didFinishLaunchingWithOptions. [SCPTerminal setTokenProvider:APIClient.shared]; } - (void)discoverReaders { NSError *configError = nil; SCPInternetDiscoveryConfiguration *config = [[[SCPInternetDiscoveryConfigurationBuilder new] setSimulated:YES] build:&configError]; if (configError) { NSLog(@"Unexpected error building discovery configuration!"); } else { self.discoverCancelable = [[SCPTerminal shared] discoverReaders:config delegate:self completion:^(NSError * _Nullable error) { NSError *configError = nil; SCPBluetoothProximityDiscoveryConfiguration *config = [[[SCPBluetoothProximityDiscoveryConfigurationBuilder new] setSimulated:YES] build:&configError]; if (configError) { NSLog(@"Unexpected error building discovery configuration!"); } else { self.discoverCancelable = [[SCPTerminal shared] discoverReaders:config delegate:self completion:^(NSError * _Nullable error) { if (error) { NSLog(@"discoverReaders failed: %@", error); self.readerMessageLabel.text = [NSString stringWithFormat:@"discoverReaders failed: %@", error.localizedDescription]; } else { NSLog(@"discoverReaders succeeded"); [self.nextActionButton setTitle:@"Make a Payment" forState:UIControlStateNormal]; [self.nextActionButton removeTarget:self action:@selector(discoverReaders) forControlEvents:UIControlEventTouchUpInside]; [self.nextActionButton addTarget:self action:@selector(collectPayment) forControlEvents:UIControlEventTouchUpInside]; } }]; } } - (void)collectPayment { NSError *error = nil; SCPPaymentIntentParameters *paymentIntentParams = [[[[SCPPaymentIntentParametersBuilder alloc] initWithAmount:1000 currency:@"{{TERMINAL_CURRENCY}}"] setPaymentMethodTypes:@[{{TERMINAL_PAYMENT_METHODS}}]] build:&error]; if (error) { NSLog(@"Error building SCPPaymentIntentParameters"); return; } [[SCPTerminal shared] createPaymentIntent:paymentIntentParams completion:^(SCPPaymentIntent * _Nullable intent, NSError * _Nullable createError) { if (createError) { NSLog(@"createPaymentIntent failed: %@", createError); self.readerMessageLabel.text = [NSString stringWithFormat:@"Error creating payment: %@", createError.localizedDescription]; } else if (intent) { NSLog(@"createPaymentIntent succeeded"); [[SCPTerminal shared] collectPaymentMethod:intent completion:^(SCPPaymentIntent * _Nullable collectIntent, NSError * _Nullable collectError) { if (collectError) { NSLog(@"collectPaymentMethod failed: %@", collectError); self.readerMessageLabel.text = [NSString stringWithFormat:@"Error collecting payment: %@", collectError.localizedDescription]; } else if (collectIntent) { NSLog(@"collectPaymentMethod succeeded"); [self confirmPaymentIntent:collectIntent]; } }]; } }]; } - (void)confirmPaymentIntent:(SCPPaymentIntent *)intent { [[SCPTerminal shared] confirmPaymentIntent:intent completion:^(SCPPaymentIntent *confirmedIntent, SCPConfirmPaymentIntentError *confirmError) { if (confirmError) { NSLog(@"confirmPaymentIntent failed: %@", confirmError); self.readerMessageLabel.text = [NSString stringWithFormat:@"Error confirming payment: %@", confirmError.localizedDescription]; } else { NSLog(@"confirmPaymentIntent succeeded"); if (confirmedIntent.stripeId != nil) { // Notify your backend to capture the PaymentIntent. // PaymentIntents confirmed with Stripe Terminal must be captured // within 24 hours of confirming the payment. [[APIClient shared] capturePaymentIntent:confirmedIntent.stripeId completion:^(NSError *captureError) { if (captureError) { NSLog(@"capture failed: %@", captureError); self.readerMessageLabel.text = [NSString stringWithFormat:@"Error capturing payment: %@", captureError.localizedDescription]; } else { NSLog(@"capture succeeded"); self.readerMessageLabel.text = @"Payment succeeded"; } }]; } else { NSLog(@"Payment collected offline"); } } }]; } - (void)setUpInterface { self.nextActionButton = [UIButton buttonWithType:UIButtonTypeSystem];; self.readerMessageLabel = [[UILabel alloc] init]; self.readerMessageLabel.textAlignment = NSTextAlignmentCenter; self.readerMessageLabel.numberOfLines = 0; [self.nextActionButton setTitle:@"Connect to a reader" forState:UIControlStateNormal]; [self.nextActionButton addTarget:self action:@selector(discoverReaders) forControlEvents:UIControlEventTouchUpInside]; UIStackView *stackView = [[UIStackView alloc] initWithArrangedSubviews:@[self.nextActionButton, self.readerMessageLabel]]; stackView.axis = UILayoutConstraintAxisVertical; stackView.translatesAutoresizingMaskIntoConstraints = false; [self.view addSubview:stackView]; [NSLayoutConstraint activateConstraints:@[ [stackView.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor], [stackView.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor], [stackView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor], [stackView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor], ]]; } #pragma mark - SCPDiscoveryDelegate - (void)terminal:(SCPTerminal *)terminal didUpdateDiscoveredReaders:(NSArray *)readers { self.readerMessageLabel.text = [NSString stringWithFormat:@"%lu readers found", readers.count]; // Select the first reader the SDK discovers. In your app, // you should display the available readers to your user, then // connect to the reader they've selected. SCPReader *selectedReader = [readers firstObject]; // Only connect if we aren't currently connected. if (!(terminal.connectionStatus == SCPConnectionStatusNotConnected || terminal.connectionStatus == SCPConnectionStatusDiscovering)) { return; } // When connecting to a physical reader, your integration should specify either the // same location as the last connection (selectedReader.locationId) or a new location // of your user's choosing. // // Since the simulated reader is not associated with a real location, we recommend // specifying its existing mock location. NSError *configError = nil; SCPBluetoothConnectionConfiguration *config = [[[SCPBluetoothConnectionConfigurationBuilder alloc] initWithDelegate:self locationId:selectedReader.locationId] build:&configError]; if (configError) { NSLog(@"Error building connection configuration, check location id!"); } else { NSError *configError = nil; SCPInternetConnectionConfiguration *config = [[[[SCPInternetConnectionConfigurationBuilder alloc] initWithDelegate:self] setFailIfInUse:YES] build:&configError]; if (configError) { NSLog(@"Error building connection configuration, check location id!"); } else { [[SCPTerminal shared] connectReader:selectedReader connectionConfig:config completion:^(SCPReader *reader, NSError *error) { if (reader != nil) { NSLog(@"Successfully connected to reader: %@", reader); self.readerMessageLabel.text = @"Connected to reader."; } else { NSLog(@"connection failed: %@", error); self.readerMessageLabel.text = [NSString stringWithFormat:@"Error connecting: %@", error.localizedDescription]; } }]; } } #pragma mark - SCPBluetoothReaderDelegate - (void)reader:(SCPReader *)reader didRequestReaderInput:(SCPReaderInputOptions)inputOptions { // Update UI requesting reader input } - (void)reader:(SCPReader *)reader didRequestReaderDisplayMessage:(SCPReaderDisplayMessage)displayMessage { // Update UI showing reader message } - (void)reader:(nonnull SCPReader *)reader didStartInstallingUpdate:(SCPReaderSoftwareUpdate *)update cancelable:(nullable SCPCancelable *)cancelable { // Show UI communicating that a required update has started installing } - (void)reader:(nonnull SCPReader *)reader didReportReaderSoftwareUpdateProgress:(float)progress { // Update the progress of the install } - (void)reader:(nonnull SCPReader *)reader didFinishInstallingUpdate:(nullable SCPReaderSoftwareUpdate *)update error:(nullable NSError *)error { // Report success or failure of the update } - (void)reader:(nonnull SCPReader *)reader didReportAvailableUpdate:(SCPReaderSoftwareUpdate *)update { // An update is available for the connected reader. Show this update in your application. // This update can be installed using Terminal.shared.installAvailableUpdate. } #pragma mark - SCPInternetReaderDelegate - (void)reader:(SCPReader *)reader didDisconnect:(SCPDisconnectReason)reason { // Handle reader disconnects here. } @end ``` ```objectivec \#import @interface ViewController : UIViewController @end ``` ```objectivec \#import "APIClient.h" @implementation APIClient static APIClient *_shared = nil; + (NSURL *) backendUrl { return [NSURL URLWithString:@"http://localhost:4242"]; } + (instancetype)shared { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _shared = [APIClient new]; }); return _shared; } - (void)fetchConnectionToken:(SCPConnectionTokenCompletionBlock)completion { NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration]; NSURLSession *session = [NSURLSession sessionWithConfiguration:config]; NSURL *url = [NSURL URLWithString:@"/connection_token" relativeToURL:[APIClient backendUrl]]; if (!url) { NSAssert(NO, @"Invalid backend URL"); } NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url]; request.HTTPMethod = @"POST"; NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { id jsonObject = nil; NSError *jsonSerializationError; if (data) { jsonObject = [NSJSONSerialization JSONObjectWithData:data options:(NSJSONReadingOptions)kNilOptions error:&jsonSerializationError]; } else { NSError *error = [NSError errorWithDomain:@"com.stripe-terminal-ios.example" code:1000 userInfo:@{NSLocalizedDescriptionKey: @"No data in response from ConnectionToken endpoint"}]; completion(nil, error); } if (!(jsonObject && [jsonObject isKindOfClass:[NSDictionary class]])) { completion(nil, jsonSerializationError); return; } NSDictionary *json = (NSDictionary *)jsonObject; id secret = json[@"secret"]; if (!(secret && [secret isKindOfClass:[NSString class]])) { NSError *error = [NSError errorWithDomain:@"com.stripe-terminal-ios.example" code:2000 userInfo:@{NSLocalizedDescriptionKey: @"Missing 'secret' in ConnectionToken JSON response"}]; completion(nil, error); return; } completion((NSString *)secret, nil); }]; [task resume]; } - (void)capturePaymentIntent:(NSString *)paymentIntentId completion:(SCPErrorCompletionBlock)completion { NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration]; NSURLSession *session = [NSURLSession sessionWithConfiguration:config]; NSURL *url = [NSURL URLWithString:@"/capture_payment_intent" relativeToURL:[APIClient backendUrl]]; NSString *parameters = [NSString stringWithFormat:@"{\"payment_intent_id\":\"%@\"}", paymentIntentId]; NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url]; request.HTTPMethod = @"POST"; request.HTTPBody = [parameters dataUsingEncoding:NSUTF8StringEncoding]; NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { if (response) { NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) response; if (httpResponse.statusCode >= 200 && httpResponse.statusCode < 300) { completion(nil); } else if (httpResponse.statusCode == 402) { NSString *description = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; if (!description) { description = @"Failed to capture payment intent"; } NSError *error = [NSError errorWithDomain:@"com.stripe-terminal-ios.example" code:2 userInfo:@{NSLocalizedDescriptionKey: description}]; completion(error); } else { if(error) { completion(error); } else { NSError *error = [NSError errorWithDomain:@"com.stripe-terminal-ios.example" code:0 userInfo:@{NSLocalizedDescriptionKey: @"Other networking error occurred"}]; completion(error); } } } else { completion(error); } }]; [task resume]; } @end ``` ```objectivec \#import #import NS_ASSUME_NONNULL_BEGIN @interface APIClient : NSObject // For simplicity, this example class is a singleton + (instancetype)shared; - (void)capturePaymentIntent:(NSString *)paymentIntentId completion:(SCPErrorCompletionBlock)completion; @end NS_ASSUME_NONNULL_END ``` ```kotlin package com.stripe.example import android.Manifest import android.bluetooth.BluetoothAdapter import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.location.LocationManager import android.os.Build import android.os.Bundle import android.provider.Settings import android.view.ContextThemeWrapper import android.view.View import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.RequiresApi import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.stripe.stripeterminal.Terminal import com.stripe.stripeterminal.external.callable.* import com.stripe.stripeterminal.external.models.DiscoveryConfiguration import com.stripe.stripeterminal.log.LogLevel import com.stripe.stripeterminal.external.models.* import retrofit2.Call import retrofit2.Response import java.lang.ref.WeakReference class MainActivity : AppCompatActivity() { // Register the permissions callback to handles the response to the system permissions dialog. private val requestPermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions(), ::onPermissionResult ) companion object { // The code that denotes the request for location permissions private const val REQUEST_CODE_LOCATION = 1 private val paymentIntentParams = PaymentIntentParameters.Builder(listOf(PaymentMethodType.CARD_PRESENT)) .setAmount(500) .setCurrency("{{TERMINAL_CURRENCY}}") .build() private val discoveryConfig = DiscoveryConfiguration.InternetDiscoveryConfiguration(isSimulated = true) private val discoveryConfig = DiscoveryConfiguration.BluetoothDiscoveryConfiguration(isSimulated = true) /*** Payment processing callbacks ***/ // (Step 1 found below in the startPayment function) // Step 2 - once we've created the payment intent, it's time to read the card private val createPaymentIntentCallback by lazy { object : PaymentIntentCallback { override fun onSuccess(paymentIntent: PaymentIntent) { Terminal.getInstance() .collectPaymentMethod(paymentIntent, collectPaymentMethodCallback) } override fun onFailure(e: TerminalException) { // Update UI w/ failure } } } // Step 3 - we've collected the payment method, so it's time to confirm the payment private val collectPaymentMethodCallback by lazy { object : PaymentIntentCallback { override fun onSuccess(paymentIntent: PaymentIntent) { Terminal.getInstance().confirmPaymentIntent(paymentIntent, confirmPaymentIntentCallback) } override fun onFailure(e: TerminalException) { // Update UI w/ failure } } } // Step 4 - we've confirmed the payment! Show a success screen private val confirmPaymentIntentCallback by lazy { object : PaymentIntentCallback { override fun onSuccess(paymentIntent: PaymentIntent) { ApiClient.capturePaymentIntent(paymentIntent.id) } override fun onFailure(e: TerminalException) { // Update UI w/ failure } } } } private val readerClickListener = ReaderClickListener(WeakReference(this)) private val readerAdapter = ReaderAdapter(readerClickListener) /** * Upon starting, we should verify we have the permissions we need, then start the app */ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) if (BluetoothAdapter.getDefaultAdapter()?.isEnabled == false) { BluetoothAdapter.getDefaultAdapter().enable() } findViewById(R.id.reader_recycler_view).apply { adapter = readerAdapter } findViewById(R.id.discover_button).setOnClickListener { discoverReaders() } findViewById(R.id.collect_payment_button).setOnClickListener { startPayment() } } override fun onResume() { super.onResume() requestPermissionsIfNecessary() } private fun isGranted(permission: String): Boolean { return ContextCompat.checkSelfPermission( this, permission ) == PackageManager.PERMISSION_GRANTED } private fun requestPermissionsIfNecessary() { if (Build.VERSION.SDK_INT >= 31) { requestPermissionsIfNecessarySdk31() } else { requestPermissionsIfNecessarySdkBelow31() } } private fun requestPermissionsIfNecessarySdkBelow31() { // Check for location permissions if (!isGranted(Manifest.permission.ACCESS_FINE_LOCATION)) { // If we don't have them yet, request them before doing anything else requestPermissionLauncher.launch(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)) } else if (!Terminal.isInitialized() && verifyGpsEnabled()) { initialize() } } @RequiresApi(Build.VERSION_CODES.S) private fun requestPermissionsIfNecessarySdk31() { // Check for location and bluetooth permissions val deniedPermissions = mutableListOf().apply { if (!isGranted(Manifest.permission.ACCESS_FINE_LOCATION)) add(Manifest.permission.ACCESS_FINE_LOCATION) if (!isGranted(Manifest.permission.BLUETOOTH_CONNECT)) add(Manifest.permission.BLUETOOTH_CONNECT) if (!isGranted(Manifest.permission.BLUETOOTH_SCAN)) add(Manifest.permission.BLUETOOTH_SCAN) }.toTypedArray() if (deniedPermissions.isNotEmpty()) { // If we don't have them yet, request them before doing anything else requestPermissionLauncher.launch(deniedPermissions) } else if (!Terminal.isInitialized() && verifyGpsEnabled()) { initialize() } } /** * Receive the result of our permissions check, and initialize if we can */ private fun onPermissionResult(result: Map) { val deniedPermissions: List = result .filter { !it.value } .map { it.key } // If we receive a response to our permission check, initialize if (deniedPermissions.isEmpty() && !Terminal.isInitialized() && verifyGpsEnabled()) { initialize() } } fun updateReaderConnection(isConnected: Boolean) { val recyclerView = findViewById(R.id.reader_recycler_view) findViewById(R.id.collect_payment_button).visibility = if (isConnected) View.VISIBLE else View.INVISIBLE findViewById(R.id.discover_button).visibility = if (isConnected) View.INVISIBLE else View.VISIBLE recyclerView.visibility = if (isConnected) View.INVISIBLE else View.VISIBLE if (!isConnected) { recyclerView.layoutManager = LinearLayoutManager(this) recyclerView.adapter = readerAdapter } } private fun initialize() { // Initialize the Terminal as soon as possible try { Terminal.initTerminal( applicationContext, LogLevel.VERBOSE, TokenProvider(), TerminalEventListener() ) } catch (e: TerminalException) { throw RuntimeException( "Location services are required in order to initialize " + "the Terminal.", e ) } val isConnectedToReader = Terminal.getInstance().connectedReader != null updateReaderConnection(isConnectedToReader) } private fun discoverReaders() { val discoveryCallback = object : Callback { override fun onSuccess() { // Update your UI } override fun onFailure(e: TerminalException) { // Update your UI } } val discoveryListener = object : DiscoveryListener { override fun onUpdateDiscoveredReaders(readers: List) { runOnUiThread { readerAdapter.updateReaders(readers) } } } Terminal.getInstance().discoverReaders(discoveryConfig, discoveryListener, discoveryCallback) } private fun startPayment() { // Step 1: create payment intent Terminal.getInstance().createPaymentIntent(paymentIntentParams, createPaymentIntentCallback) } private fun verifyGpsEnabled(): Boolean { val locationManager: LocationManager? = applicationContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager? var gpsEnabled = false try { gpsEnabled = locationManager?.isProviderEnabled(LocationManager.GPS_PROVIDER) ?: false } catch (exception: Exception) {} if (!gpsEnabled) { // notify user AlertDialog.Builder(ContextThemeWrapper(this, R.style.Theme_MaterialComponents_DayNight_DarkActionBar)) .setMessage("Please enable location services") .setCancelable(false) .setPositiveButton("Open location settings") { param, paramInt -> this.startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)) } .create() .show() } return gpsEnabled } } ``` ```kotlin package com.stripe.example import com.stripe.stripeterminal.external.callable.ConnectionTokenCallback import com.stripe.stripeterminal.external.callable.ConnectionTokenProvider import com.stripe.stripeterminal.external.models.ConnectionTokenException // A simple implementation of the [ConnectionTokenProvider] interface. We just request a // new token from our backend simulator and forward any exceptions along to the SDK. class TokenProvider : ConnectionTokenProvider { override fun fetchConnectionToken(callback: ConnectionTokenCallback) { try { val token = ApiClient.createConnectionToken() callback.onSuccess(token) } catch (e: ConnectionTokenException) { callback.onFailure(e) } } } ``` ```kotlin package com.stripe.example import android.app.Application import androidx.lifecycle.ProcessLifecycleOwner import com.stripe.stripeterminal.TerminalApplicationDelegate class StripeTerminalApplication : Application() { override fun onCreate() { super.onCreate() // If you already have a class that extends 'Application', // put whatever code you had in the 'onCreate' method here. TerminalApplicationDelegate.onCreate(this) } } ``` ```kotlin package com.stripe.example import android.view.LayoutInflater import android.view.ViewGroup import android.widget.Toast import androidx.recyclerview.widget.RecyclerView import com.google.android.material.button.MaterialButton import com.stripe.stripeterminal.Terminal import com.stripe.stripeterminal.external.callable.ReaderCallback import com.stripe.stripeterminal.external.models.ConnectionConfiguration import com.stripe.stripeterminal.external.models.DisconnectReason import com.stripe.stripeterminal.external.callable.InternetReaderListener import com.stripe.stripeterminal.external.models.Reader import com.stripe.stripeterminal.external.models.TerminalException import java.lang.ref.WeakReference // A simple [RecyclerView.ViewHolder] that contains a representation of each discovered reader class ReaderHolder(val view: MaterialButton) : RecyclerView.ViewHolder(view) // Our [RecyclerView.Adapter] implementation that allows us to update the list of readers class ReaderAdapter( private val clickListener: ReaderClickListener ) : RecyclerView.Adapter() { private var readers: List = listOf() fun updateReaders(readers: List) { this.readers = readers notifyDataSetChanged() } override fun getItemCount(): Int { return readers.size } override fun onBindViewHolder(holder: ReaderHolder, position: Int) { holder.view.text = readers[position].serialNumber holder.view.setOnClickListener { clickListener.onClick(readers[position]) } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ReaderHolder { val view = LayoutInflater.from(parent.context) .inflate(R.layout.list_item_reader, parent, false) as MaterialButton return ReaderHolder(view) } } class ReaderClickListener(val activityRef: WeakReference) { fun onClick(reader: Reader) { // When connecting to a physical reader, your integration should specify either the // same location as the last connection (reader.locationId) or a new location // of your user's choosing. // // Since the simulated reader is not associated with a real location, we recommend // specifying its existing mock location. val connectionConfig = ConnectionConfiguration.BluetoothConnectionConfiguration( locationId = reader.location!!.id!!, autoReconnectOnUnexpectedDisconnect = true, bluetoothReaderListener = TerminalBluetoothReaderListener(), ) val connectionConfig = ConnectionConfiguration.InternetConnectionConfiguration( failIfInUse = true, internetReaderListener = object : InternetReaderListener { override fun onDisconnect(reason: DisconnectReason) { // Show UI that your reader disconnected } }, ) val readerCallback = object: ReaderCallback { override fun onSuccess(reader: Reader) { activityRef.get()?.let { it.runOnUiThread { // Update UI with connection success it.updateReaderConnection(isConnected = true) } } } override fun onFailure(e: TerminalException) { activityRef.get()?.let { it.runOnUiThread { // Update UI with connection failure Toast.makeText( it, "Failed to connect to reader", Toast.LENGTH_SHORT) .show() } } } } Terminal.getInstance().connectReader( reader = reader, config = connectionConfig, connectionCallback = readerCallback, ) } } ``` ```kotlin package com.stripe.example import com.facebook.stetho.okhttp3.StethoInterceptor import com.stripe.stripeterminal.external.models.ConnectionTokenException import okhttp3.OkHttpClient import retrofit2.Callback import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import java.io.IOException // The 'ApiClient' is a singleton object used to make calls to our backend and return their results object ApiClient { const val BACKEND_URL = "http://localhost:4242" private val client = OkHttpClient.Builder() .addNetworkInterceptor(StethoInterceptor()) .build() private val retrofit: Retrofit = Retrofit.Builder() .baseUrl(BACKEND_URL) .client(client) .addConverterFactory(GsonConverterFactory.create()) .build() private val service: BackendService = retrofit.create(BackendService::class.java) @Throws(ConnectionTokenException::class) internal fun createConnectionToken(): String { try { val result = service.getConnectionToken().execute() if (result.isSuccessful && result.body() != null) { return result.body()!!.secret } else { throw ConnectionTokenException("Creating connection token failed") } } catch (e: IOException) { throw ConnectionTokenException("Creating connection token failed", e) } } @Throws(Exception::class) internal fun createPaymentIntent( amount: Int, currency: String, callback: Callback ) { service.createPaymentIntent(amount, currency).enqueue(callback) } internal fun capturePaymentIntent(id: String) { service.capturePaymentIntent(id).execute() } } ``` ```kotlin package com.stripe.example import com.stripe.example.ConnectionToken import com.stripe.example.ServerPaymentIntent import retrofit2.Call import retrofit2.http.Field import retrofit2.http.FormUrlEncoded import retrofit2.http.POST /** * The 'BackendService' interface handles the two simple calls we need to make to our backend. * This represents YOUR backend, so feel free to change the routes accordingly. */ interface BackendService { /** * Get a connection token string from the backend */ @POST("connection_token") fun getConnectionToken(): Call /** * Create a payment intent on the backend */ @FormUrlEncoded @POST("create_payment_intent") fun createPaymentIntent( @Field("amount") amount: Int, @Field("currency") currency: String ): Call /** * Capture a specific payment intent on our backend */ @FormUrlEncoded @POST("capture_payment_intent") fun capturePaymentIntent(@Field("payment_intent_id") id: String): Call } ``` ```kotlin package com.stripe.example import com.stripe.stripeterminal.external.callable.TerminalListener import com.stripe.stripeterminal.external.models.ConnectionStatus import com.stripe.stripeterminal.external.models.PaymentStatus class TerminalEventListener : TerminalListener { override fun onConnectionStatusChange(status: ConnectionStatus) { } override fun onPaymentStatusChange(status: PaymentStatus) { } } ``` ```kotlin package com.stripe.example import com.stripe.stripeterminal.external.callable.Cancelable import com.stripe.stripeterminal.external.callable.MobileReaderListener import com.stripe.stripeterminal.external.models.BatteryStatus import com.stripe.stripeterminal.external.models.DisconnectReason import com.stripe.stripeterminal.external.models.Reader import com.stripe.stripeterminal.external.models.ReaderEvent import com.stripe.stripeterminal.external.models.ReaderDisplayMessage import com.stripe.stripeterminal.external.models.ReaderInputOptions import com.stripe.stripeterminal.external.models.ReaderSoftwareUpdate import com.stripe.stripeterminal.external.models.TerminalException /** * Use the [MobileReaderListener] interface for the entire duration of your connection to a reader. * It receives events related to the reader's status and provides opportunities for you to update * the reader's software. */ class TerminalBluetoothReaderListener : MobileReaderListener { override fun onDisconnect(reason: DisconnectReason) { // Show the UI that indicates your reader has disconnected } override fun onReportAvailableUpdate(update: ReaderSoftwareUpdate) { // Check if an update is available } override fun onStartInstallingUpdate(update: ReaderSoftwareUpdate, cancelable: Cancelable?) { // Show the UI indicating that the required update has started installing } override fun onReportReaderSoftwareUpdateProgress(progress: Float) { // Update the progress of the install } override fun onFinishInstallingUpdate(update: ReaderSoftwareUpdate?, e: TerminalException?) { // Report success or failure of the update } override fun onRequestReaderInput(options: ReaderInputOptions) { } override fun onRequestReaderDisplayMessage(message: ReaderDisplayMessage) { } override fun onReportLowBatteryWarning() { } override fun onBatteryLevelUpdate(batteryLevel: Float, batteryStatus: BatteryStatus, isCharging: Boolean) { } override fun onReportReaderEvent(event: ReaderEvent) { } override fun onReaderReconnectStarted(reader: Reader, cancelReconnect: Cancelable, reason: DisconnectReason) { } override fun onReaderReconnectSucceeded(reader: Reader) { } override fun onReaderReconnectFailed(reader: Reader) { } } ``` ```kotlin package com.stripe.example // A one-field data class used to handle the connection token response from our backend data class ConnectionToken(val secret: String) ``` ```kotlin package com.stripe.example data class ServerPaymentIntent(val intent: String, val secret: String) ``` ```xml ``` ```xml ``` ```xml ``` ```groovy plugins { id 'com.android.application' id 'kotlin-android' id 'kotlin-kapt' } android { compileSdkVersion 31 defaultConfig { minSdkVersion 29 targetSdkVersion 29 } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '1.8' } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "com.google.android.material:material:1.1.0" implementation "androidx.appcompat:appcompat:1.3.1" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.4.10" implementation "androidx.constraintlayout:constraintlayout:2.0.2" implementation "com.stripe:stripeterminal:4.3.1" implementation "com.facebook.stetho:stetho-okhttp3:1.5.1" implementation "com.squareup.retrofit2:retrofit:2.9.0" implementation "com.squareup.retrofit2:converter-gson:2.9.0" implementation "androidx.lifecycle:lifecycle-runtime:2.2.0" implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" kapt "androidx.lifecycle:lifecycle-compiler:2.2.0" } ``` ```java package com.stripe.example; import android.Manifest; import android.bluetooth.BluetoothAdapter; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.location.LocationManager; import android.os.Build; import android.os.Bundle; import android.provider.Settings; import android.view.ContextThemeWrapper; import android.view.View; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.stripe.stripeterminal.Terminal; import com.stripe.stripeterminal.external.callable.Callback; import com.stripe.stripeterminal.external.callable.DiscoveryListener; import com.stripe.stripeterminal.external.callable.PaymentIntentCallback; import com.stripe.stripeterminal.external.models.DiscoveryConfiguration; import com.stripe.stripeterminal.external.models.DiscoveryMethod; import com.stripe.stripeterminal.external.models.PaymentIntent; import com.stripe.stripeterminal.external.models.PaymentIntentParameters; import com.stripe.stripeterminal.external.models.PaymentMethodType; import com.stripe.stripeterminal.external.models.Reader; import com.stripe.stripeterminal.external.models.TerminalException; import com.stripe.stripeterminal.log.LogLevel; import java.io.IOException; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import javax.annotation.Nullable; import retrofit2.Call; import retrofit2.Response; public class MainActivity extends AppCompatActivity { private final int REQUEST_CODE_LOCATION = 1; // Register the permissions callback to handles the response to the system permissions dialog. private final ActivityResultLauncher requestPermissionLauncher = registerForActivityResult( new ActivityResultContracts.RequestMultiplePermissions(), result -> MainActivity.this.onPermissionResult(result) ); private final PaymentIntentParameters paymentIntentParams = new PaymentIntentParameters.Builder(Arrays.asList({{TERMINAL_PAYMENT_METHODS}})) .setAmount(500) .setCurrency("{{TERMINAL_CURRENCY}}") .build(); private final DiscoveryConfiguration discoveryConfig = new DiscoveryConfiguration.InternetDiscoveryConfiguration(null, true); private final DiscoveryConfiguration discoveryConfig = new DiscoveryConfiguration.BluetoothDiscoveryConfiguration(0, true); private final ReaderClickListener readerClickListener = new ReaderClickListener(new WeakReference(this)); private final ReaderAdapter readerAdapter = new ReaderAdapter(readerClickListener); /*** Payment processing callbacks ***/ // (Step 1 found below in the startPayment function) // Step 2 - once we've created the payment intent, it's time to read the card private final PaymentIntentCallback createPaymentIntentCallback = new PaymentIntentCallback() { @Override public void onSuccess(@NonNull PaymentIntent paymentIntent) { Terminal.getInstance() .collectPaymentMethod(paymentIntent, collectPaymentMethodCallback); } @Override public void onFailure(@NonNull TerminalException e) { // Update UI w/ failure } }; // Step 3 - we've collected the payment method, so it's time to confirm the payment private final PaymentIntentCallback collectPaymentMethodCallback = new PaymentIntentCallback() { @Override public void onSuccess(@NonNull PaymentIntent paymentIntent) { Terminal.getInstance().confirmPaymentIntent(paymentIntent, confirmPaymentIntentCallback); } @Override public void onFailure(@NonNull TerminalException e) { // Update UI w/ failure } }; // Step 4 - we've confirmed the payment! Show a success screen private final PaymentIntentCallback confirmPaymentIntentCallback = new PaymentIntentCallback() { @Override public void onSuccess(@NonNull PaymentIntent paymentIntent) { try { ApiClient.capturePaymentIntent(paymentIntent.getId()); } catch (IOException e) { throw new RuntimeException(e); } runOnUiThread(() -> { new AlertDialog.Builder(new ContextThemeWrapper(MainActivity.this, R.style.Theme_MaterialComponents_DayNight_DarkActionBar)) .setMessage("Successfully captured payment!") .setCancelable(true) .create() .show(); }); } @Override public void onFailure(@NonNull TerminalException e) { // Update UI w/ failure } }; private void startPayment() { // Step 1: create payment intent Terminal.getInstance() .createPaymentIntent(paymentIntentParams, createPaymentIntentCallback); } private boolean verifyGpsEnabled() { final LocationManager locationManager = (LocationManager) getApplicationContext() .getSystemService(Context.LOCATION_SERVICE); boolean gpsEnabled = false; try { if (locationManager != null) { gpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER); } } catch (Exception e) {} if (!gpsEnabled) { // notify user new AlertDialog.Builder(new ContextThemeWrapper(this, R.style.Theme_MaterialComponents_DayNight_DarkActionBar)) .setMessage("Please enable location services") .setCancelable(false) .setPositiveButton("Open location settings", (dialog, which) -> startActivity(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS))) .create() .show(); } return gpsEnabled; } private void initialize() { // Initialize the Terminal as soon as possible try { Terminal.initTerminal( getApplicationContext(), LogLevel.VERBOSE, new TokenProvider(), new TerminalEventListener()); } catch (TerminalException e) { throw new RuntimeException( "Location services are required in order to initialize " + "the Terminal.", e ); } final boolean isConnectedToReader = Terminal.getInstance().getConnectedReader() != null; updateReaderConnection(isConnectedToReader); } private void discoverReaders() { final Callback discoveryCallback = new Callback() { @Override public void onSuccess() { // Update your UI } @Override public void onFailure(@NonNull TerminalException e) { // Update your UI } }; final DiscoveryListener discoveryListener = new DiscoveryListener() { @Override public void onUpdateDiscoveredReaders(@NonNull List readers) { runOnUiThread(() -> readerAdapter.updateReaders(readers)); } }; Terminal.getInstance() .discoverReaders(discoveryConfig, discoveryListener, discoveryCallback); } void updateReaderConnection(boolean isConnected) { final RecyclerView recyclerView = findViewById(R.id.reader_recycler_view); findViewById(R.id.collect_payment_button) .setVisibility(isConnected ? View.VISIBLE : View.INVISIBLE); findViewById(R.id.discover_button) .setVisibility(isConnected ? View.INVISIBLE : View.VISIBLE); recyclerView.setVisibility(isConnected ? View.INVISIBLE : View.VISIBLE); if (!isConnected) { recyclerView.setLayoutManager(new LinearLayoutManager(this)); recyclerView.setAdapter(readerAdapter); } } // Upon starting, we should verify we have the permissions we need, then start the app @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if (BluetoothAdapter.getDefaultAdapter() != null && !BluetoothAdapter.getDefaultAdapter().isEnabled()) { BluetoothAdapter.getDefaultAdapter().enable(); } findViewById(R.id.discover_button).setOnClickListener(v -> discoverReaders()); findViewById(R.id.collect_payment_button).setOnClickListener(v -> startPayment()); } @Override public void onResume() { super.onResume(); requestPermissionsIfNecessary(); } private Boolean isGranted(String permission) { return ContextCompat.checkSelfPermission( this, permission ) == PackageManager.PERMISSION_GRANTED; } private void requestPermissionsIfNecessary() { if (Build.VERSION.SDK_INT >= 31) { requestPermissionsIfNecessarySdk31(); } else { requestPermissionsIfNecessarySdkBelow31(); } } private void requestPermissionsIfNecessarySdkBelow31() { // Check for location permissions if (!isGranted(Manifest.permission.ACCESS_FINE_LOCATION)) { // If we don't have them yet, request them before doing anything else requestPermissionLauncher.launch(Arrays.asList(Manifest.permission.ACCESS_FINE_LOCATION)); } else if (!Terminal.isInitialized() && verifyGpsEnabled()) { initialize(); } } @RequiresApi(Build.VERSION_CODES.S) private void requestPermissionsIfNecessarySdk31() { // Check for location and bluetooth permissions List deniedPermissions = new ArrayList<>(); if (!isGranted(Manifest.permission.ACCESS_FINE_LOCATION)) { deniedPermissions.add(Manifest.permission.ACCESS_FINE_LOCATION); } if (!isGranted(Manifest.permission.BLUETOOTH_CONNECT)) { deniedPermissions.add(Manifest.permission.BLUETOOTH_CONNECT); } if (!isGranted(Manifest.permission.BLUETOOTH_SCAN)) { deniedPermissions.add(Manifest.permission.BLUETOOTH_SCAN); } if (!deniedPermissions.isEmpty()) { // If we don't have them yet, request them before doing anything else requestPermissionLauncher.launch(deniedPermissions.toArray(new String[deniedPermissions.size()])); } else if (!Terminal.isInitialized() && verifyGpsEnabled()) { initialize(); } } /** * Receive the result of our permissions check, and initialize if we can */ void onPermissionResult(Map result) { List deniedPermissions = result.entrySet().stream() .filter(it -> !it.getValue()) .map(it -> it.getKey()) .collect(Collectors.toList()); // If we receive a response to our permission check, initialize if (deniedPermissions.isEmpty() && !Terminal.isInitialized() && verifyGpsEnabled()) { initialize(); } } } ``` ```java package com.stripe.example; import com.stripe.stripeterminal.external.callable.ConnectionTokenCallback; import com.stripe.stripeterminal.external.callable.ConnectionTokenProvider; import com.stripe.stripeterminal.external.models.ConnectionTokenException; /** * A simple implementation of the [ConnectionTokenProvider] interface. We just request a * new token from our backend simulator and forward any exceptions along to the SDK. */ public class TokenProvider implements ConnectionTokenProvider { @Override public void fetchConnectionToken(ConnectionTokenCallback callback) { try { final String token = ApiClient.createConnectionToken(); callback.onSuccess(token); } catch (ConnectionTokenException e) { callback.onFailure(e); } } } ``` ```java package com.stripe.example; import android.app.Application; import androidx.lifecycle.ProcessLifecycleOwner; import com.stripe.stripeterminal.TerminalApplicationDelegate; public class StripeTerminalApplication extends Application { @Override public void onCreate() { super.onCreate(); // If you already have a class that extends 'Application', // put whatever code you had in the 'onCreate' method here. TerminalApplicationDelegate.onCreate(this); } } ``` ```java package com.stripe.example; import androidx.annotation.NonNull; import com.stripe.stripeterminal.Terminal; import com.stripe.stripeterminal.external.callable.InternetReaderListener; import com.stripe.stripeterminal.external.callable.ReaderCallback; import com.stripe.stripeterminal.external.models.ConnectionConfiguration; import com.stripe.stripeterminal.external.models.DisconnectReason; import com.stripe.stripeterminal.external.models.Reader; import com.stripe.stripeterminal.external.models.TerminalException; import java.lang.ref.WeakReference; public class ReaderClickListener { @NonNull private final WeakReference activityRef; public ReaderClickListener(@NonNull WeakReference weakReference) { activityRef = weakReference; } public void onClick(@NonNull Reader reader) { // When connecting to a physical reader, your integration should specify either the // same location as the last connection (reader.locationId) or a new location // of your user's choosing. // // Since the simulated reader is not associated with a real location, we recommend // specifying its existing mock location. String locationId; if (reader.getLocation() != null) { locationId = reader.getLocation().getId(); } else { // The reader is not associated with a location. Insert business logic here to determine // where the reader should be registered, and pass the location ID to the reader. throw new RuntimeException("No location ID available"); } ConnectionConfiguration.InternetConnectionConfiguration connectionConfig = new ConnectionConfiguration.InternetConnectionConfiguration( true, new InternetReaderListener() { @Override public void onDisconnect(@NonNull DisconnectReason reason) { final MainActivity activity = activityRef.get(); if (activity != null) { activity.runOnUiThread(() -> { // Show UI that your reader disconnected }); } } } ); ConnectionConfiguration.BluetoothConnectionConfiguration connectionConfig = new ConnectionConfiguration.BluetoothConnectionConfiguration( locationId, true, new TerminalBluetoothReaderListener() ); Terminal.getInstance().connectReader( reader, connectionConfig, new ReaderCallback() { @Override public void onSuccess(@NonNull Reader reader) { final MainActivity activity = activityRef.get(); if (activity != null) { activity.runOnUiThread(() -> { // Update UI w/ connection success activity.updateReaderConnection(true); }); } } @Override public void onFailure(@NonNull TerminalException e) { final MainActivity activity = activityRef.get(); if (activity != null) { activity.runOnUiThread(() -> { // Update UI w/ connection failure }); } } } ); Terminal.getInstance().connectReader( reader, connectionConfig, new ReaderCallback() { @Override public void onSuccess(@NonNull Reader reader) { final MainActivity activity = activityRef.get(); if (activity != null) { activity.runOnUiThread(() -> { // Update UI w/ connection success activity.updateReaderConnection(true); }); } } @Override public void onFailure(@NonNull TerminalException e) { final MainActivity activity = activityRef.get(); if (activity != null) { activity.runOnUiThread(() -> { // Update UI w/ connection failure }); } } } ); } } ``` ```java package com.stripe.example; import androidx.annotation.NonNull; import com.facebook.stetho.okhttp3.StethoInterceptor; import com.stripe.stripeterminal.external.models.ConnectionTokenException; import java.io.IOException; import okhttp3.OkHttpClient; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; public class ApiClient { // Use 10.0.2.2 when running with an emulator // See https://developer.android.com/studio/run/emulator-networking.html#networkaddresses public static final String BACKEND_URL = "http://localhost:4242"; private static final OkHttpClient mClient = new OkHttpClient.Builder() .addNetworkInterceptor(new StethoInterceptor()) .build(); private static final Retrofit mRetrofit = new Retrofit.Builder() .baseUrl(BACKEND_URL) .client(mClient) .addConverterFactory(GsonConverterFactory.create()) .build(); private static final BackendService mService = mRetrofit.create(BackendService.class); public static String createConnectionToken() throws ConnectionTokenException { try { final Response result = mService.getConnectionToken().execute(); if (result.isSuccessful() && result.body() != null) { return result.body().getSecret(); } else { throw new ConnectionTokenException("Creating connection token failed"); } } catch (IOException e) { throw new ConnectionTokenException("Creating connection token failed", e); } } public static void createPaymentIntent( Integer amount, String currency, Callback callback) { mService.createPaymentIntent(amount, currency).enqueue(callback); } public static void capturePaymentIntent(@NonNull String id) throws IOException { mService.capturePaymentIntent(id).execute(); } } ``` ```java package com.stripe.example; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.button.MaterialButton; // A simple [RecyclerView.ViewHolder] that contains a representation of each discovered reader public class ReaderHolder extends RecyclerView.ViewHolder { public final MaterialButton view; public ReaderHolder(@NonNull MaterialButton view) { super(view); this.view = view; } } ``` ```java package com.stripe.example; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.button.MaterialButton; import com.stripe.stripeterminal.external.models.Reader; import java.util.ArrayList; import java.util.List; // Our [RecyclerView.Adapter] implementation that allows us to update the list of readers public class ReaderAdapter extends RecyclerView.Adapter { @NonNull private List readers; @NonNull private ReaderClickListener clickListener; public ReaderAdapter(@NonNull ReaderClickListener clickListener) { super(); readers = new ArrayList(); this.clickListener = clickListener; } public void updateReaders(@NonNull List readers) { this.readers = readers; notifyDataSetChanged(); } @Override public int getItemCount() { return readers.size(); } @Override public void onBindViewHolder(@NonNull ReaderHolder holder, int position) { holder.view.setText(readers.get(position).getSerialNumber()); holder.view.setOnClickListener(v -> { clickListener.onClick(readers.get(position)); }); } @Override public ReaderHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { final View view = LayoutInflater.from(parent.getContext()) .inflate(R.layout.list_item_reader, parent, false); return new ReaderHolder((MaterialButton) view); } } ``` ```java package com.stripe.example; import androidx.annotation.NonNull; import retrofit2.Call; import retrofit2.http.Field; import retrofit2.http.FormUrlEncoded; import retrofit2.http.POST; /** * The 'BackendService' interface handles the two simple calls we need to make to our backend. * This represents YOUR backend, so feel free to change the routes accordingly. */ public interface BackendService { /** * Get a connection token string from the backend */ @POST("connection_token") Call getConnectionToken(); /** * Create a payment intent on the backend */ @FormUrlEncoded @POST("create_payment_intent") Call createPaymentIntent( @Field("amount") Integer amount, @Field("currency") String currency ); /** * Capture a specific payment intent on our backend */ @FormUrlEncoded @POST("capture_payment_intent") Call capturePaymentIntent(@Field("payment_intent_id") @NotNull String id); } ``` ```java package com.stripe.example; import androidx.annotation.NonNull; // A one-field data class used to handle the connection token response from our backend public class ConnectionToken { @NonNull private final String secret; public ConnectionToken(@NonNull String secret) { this.secret = secret; } @NonNull public String getSecret() { return secret; } } ``` ```java package com.stripe.example; import androidx.annotation.NonNull; import retrofit2.Call; import retrofit2.http.Field; import retrofit2.http.FormUrlEncoded; import retrofit2.http.POST; /** * The 'BackendService' interface handles the two simple calls we need to make to our backend. * This represents YOUR backend, so feel free to change the routes accordingly. */ public interface BackendService { /** * Get a connection token string from the backend */ @POST("connection_token") Call getConnectionToken(); /** * Create a payment intent on the backend */ @FormUrlEncoded @POST("create_payment_intent") Call createPaymentIntent( @Field("amount") Integer amount, @Field("currency") String currency ); /** * Capture a specific payment intent on our backend */ @FormUrlEncoded @POST("capture_payment_intent") Call capturePaymentIntent(@Field("payment_intent_id") @NotNull String id); } ``` ```java package com.stripe.example; import org.jetbrains.annotations.NotNull; // A one-field data class used to handle the payment intent response from our backend public class ServerPaymentIntent { @NotNull private final String secret; public ServerPaymentIntent(@NotNull String secret) { this.secret = secret; } @NotNull public String getSecret() { return secret; } } ``` ```java package com.stripe.example; import androidx.annotation.NonNull; import com.stripe.stripeterminal.external.callable.TerminalListener; import com.stripe.stripeterminal.external.models.ConnectionStatus; import com.stripe.stripeterminal.external.models.PaymentStatus; public class TerminalEventListener implements TerminalListener { @Override public void onConnectionStatusChange(@NonNull ConnectionStatus connectionStatus) {} @Override public void onPaymentStatusChange(@NonNull PaymentStatus paymentStatus) {} } ``` ```java package com.stripe.example; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.stripe.stripeterminal.external.callable.Cancelable; import com.stripe.stripeterminal.external.callable.MobileReaderListener; import com.stripe.stripeterminal.external.models.BatteryStatus; import com.stripe.stripeterminal.external.models.DisconnectReason; import com.stripe.stripeterminal.external.models.Reader; import com.stripe.stripeterminal.external.models.ReaderEvent; import com.stripe.stripeterminal.external.models.ReaderDisplayMessage; import com.stripe.stripeterminal.external.models.ReaderInputOptions; import com.stripe.stripeterminal.external.models.ReaderSoftwareUpdate; import com.stripe.stripeterminal.external.models.TerminalException; /** * Use the [MobileReaderListener] interface for the entire duration of your connection to a reader. * It receives events related to the reader's status and provides opportunities for you to update * the reader's software. */ public class TerminalBluetoothReaderListener implements MobileReaderListener { @Override public void onDisconnect(@NonNull DisconnectReason reason) { // Show the UI that indicates your reader has disconnected } @Override public void onReportAvailableUpdate(@NonNull ReaderSoftwareUpdate readerSoftwareUpdate) { // Check if an update is available } @Override public void onStartInstallingUpdate(@NonNull ReaderSoftwareUpdate update, @Nullable Cancelable cancelable) { // Show the UI indicating that the required update has started installing } @Override public void onReportReaderSoftwareUpdateProgress(float progress) { // Update the progress of the install } @Override public void onFinishInstallingUpdate(@Nullable ReaderSoftwareUpdate update, @Nullable TerminalException e) { // Report success or failure of the update } @Override public void onRequestReaderInput(@NonNull ReaderInputOptions readerInputOptions) {} @Override public void onRequestReaderDisplayMessage(@NonNull ReaderDisplayMessage readerDisplayMessage) {} @Override public void onReportLowBatteryWarning() {} @Override public void onBatteryLevelUpdate(float batteryLevel, @NonNull BatteryStatus batteryStatus, boolean isCharging) {} @Override public void onReportReaderEvent(@NonNull ReaderEvent readerEvent) {} @Override public void onReaderReconnectStarted(@NonNull Reader reader, @NonNull Cancelable cancelReconnect, @NonNull DisconnectReason reason) {} @Override public void onReaderReconnectSucceeded(@NonNull Reader reader) {} @Override public void onReaderReconnectFailed(@NonNull Reader reader) {} } ``` ```xml ``` ```xml ``` ```xml ``` ```groovy plugins { id 'com.android.application' } android { compileSdkVersion 31 buildToolsVersion "30.0.2" defaultConfig { applicationId "com.stripe.example" minSdkVersion 24 targetSdkVersion 30 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.appcompat:appcompat:1.3.1' implementation 'com.google.android.material:material:1.2.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.navigation:navigation-fragment:2.3.1' implementation 'androidx.navigation:navigation-ui:2.3.1' implementation "com.stripe:stripeterminal:4.3.1" implementation 'com.facebook.stetho:stetho-okhttp3:1.5.1' implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' implementation "androidx.lifecycle:lifecycle-runtime:2.2.0" implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" annotationProcessor "androidx.lifecycle:lifecycle-compiler:2.2.0" testImplementation 'junit:junit:4.+' androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' } ``` ``` # Accept in-person payments Set up the Stripe Terminal SDK so you can begin accepting in-person payments. Included are some basic build and run scripts you can use to start up the application. ## Running the sample 1. Build the server ~~~ npm install ~~~ 2. Run the server ~~~ npm start ~~~ 1. Run the server ~~~ go run server.go ~~~ 1. Build the server ~~~ pip3 install -r requirements.txt ~~~ 2. Run the server ~~~ export FLASK_APP=server.py python3 -m flask run --port=4242 ~~~ 1. Build the server ~~~ bundle install ~~~ 2. Run the server ~~~ ruby server.rb -o 0.0.0.0 ~~~ 1. Build the server ~~~ composer install ~~~ 2. Run the server ~~~ php -S 127.0.0.1:4242 router.php ~~~ 1. Build the server ~~~ dotnet restore ~~~ 2. Run the server ~~~ dotnet run ~~~ 1. Build the server ~~~ mvn package ~~~ 2. Run the server ~~~ java -cp target/sample-jar-with-dependencies.jar com.stripe.sample.Server ~~~ 3. Go to [http://localhost:4242](http://localhost:4242) ``` ## See Also #### [Connecting to a reader](https://docs.stripe.com/terminal/payments/connect-reader.md) Learn what it means to connect your app to a reader. #### [Fleet management](https://docs.stripe.com/terminal/fleet/locations-and-zones.md) Group and manage a fleet of readers by physical location. #### [Connect](https://docs.stripe.com/terminal/features/connect.md) Integrate Stripe Terminal with your Connect platform.