├── .gitignore
├── CONTRIBUTING.md
├── Dockerfile
├── Gemfile
├── Gemfile.lock
├── LICENSE.md
├── Procfile
├── README.md
├── api
├── README.md
├── dotnet
│ ├── .dockerignore
│ ├── Controllers
│ │ └── AppController.cs
│ ├── Dockerfile
│ ├── Program.cs
│ ├── Properties
│ │ └── launchSettings.json
│ ├── README.md
│ ├── Startup.cs
│ ├── appsettings.Development.json
│ ├── appsettings.json
│ ├── docker-compose.yml
│ └── dotnet.csproj
├── go
│ ├── Dockerfile
│ ├── README.md
│ ├── docker-compose.yml
│ ├── go.mod
│ ├── go.sum
│ └── main.go
├── java
│ ├── Dockerfile
│ ├── README.md
│ ├── docker-compose.yml
│ ├── pom.xml
│ └── src
│ │ └── main
│ │ └── java
│ │ └── com
│ │ └── recurly
│ │ └── App.java
├── netlify
│ ├── functions
│ │ ├── config.js
│ │ ├── new-account.js
│ │ ├── new-subscription.js
│ │ └── update-account.js
│ ├── package-lock.json
│ └── package.json
├── node
│ ├── Dockerfile
│ ├── README.md
│ ├── app.js
│ ├── docker-compose.yml
│ ├── package-lock.json
│ └── package.json
├── php
│ ├── Dockerfile
│ ├── README.md
│ ├── app.php
│ ├── composer.json
│ ├── composer.lock
│ └── docker-compose.yml
├── python
│ ├── Dockerfile
│ ├── README.md
│ ├── app.py
│ ├── docker-compose.yml
│ └── requirements.txt
└── ruby
│ ├── Dockerfile
│ ├── Gemfile
│ ├── Gemfile.lock
│ ├── README.md
│ ├── app.rb
│ └── docker-compose.yml
├── app.json
├── docker.env
├── electron
├── app.js
├── package-lock.json
├── package.json
└── readme.md
├── netlify.toml
└── public
├── 3d-secure
├── authenticate.html
└── index.html
├── README.md
├── account-update
├── done-white-18dp.svg
└── index.html
├── advanced-bank-account
└── index.html
├── advanced-pricing-items
└── index.html
├── advanced-pricing
└── index.html
├── advanced-tax
└── index.html
├── adyen
└── index.html
├── alternative-payment-methods
├── boleto.html
├── completed.html
└── ideal.html
├── apple-pay
├── advanced
│ └── index.html
├── direct-tax
│ └── index.html
├── index.html
└── shipping
│ └── index.html
├── auto-tab
└── index.html
├── bank-redirect
└── index.html
├── coBadged
└── index.html
├── fraud-detection
├── README.md
├── braintree.html
└── recurly-kount.html
├── gift-cards
└── index.html
├── google-pay
├── direct-tax
│ └── index.html
└── index.html
├── index.html
├── minimal
└── index.html
├── paypal
└── index.html
├── style.css
└── vue
└── index.html
/.gitignore:
--------------------------------------------------------------------------------
1 | /api/php/vendor/
2 | /api/netlify/node_modules/
3 | /api/node/node_modules/
4 | /api/dotnet/[Dd]ebug/
5 | /api/dotnet/[Rr]elease/
6 | /api/dotnet/x64/
7 | /api/dotnet/[Bb]in/
8 | /api/dotnet/[Oo]bj/
9 | /api/dotnet/.vscode/
10 | /api/dotnet/wwwroot
11 | /api/php/composer.phar
12 | /api/java/.java-version
13 | /api/java/target
14 | /electron/node_modules/
15 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | ### Contributing to this project
4 |
5 | We love contributors! This project is structured in a way that makes it quite easy to
6 | expand both frontend and language-specific backends. Each backend example serves
7 | the same set of frontend examples; thus, any new frontend example will work
8 | with any of the backend servers immediately.
9 |
10 | If you're uncertain about any of the guidelines or want help making a contribution, we're
11 | glad to assist. Just [create a Pull Request][new-pr] with your proposal and we'll be happy to
12 | jump in and help.
13 |
14 | #### Creating new frontend examples
15 |
16 | 1. Create a new directory to contain your example in the [public folder](public). Keep all of your HTML, CSS, and JS within this directory.
17 | 2. Depending on what action you want your form to take, submit it to the relevant endpoint in the [API server specifications](#api-server-specifications).
18 | 3. Update [index.html](public/index.html) to link to your new example.
19 | 4. Update the [README](README.md) to link to the code directory of your new example.
20 |
21 | #### Creating new backend examples
22 |
23 | 1. Create a new directory in the [api directory](api), named after the language you wish to add.
24 | 2. Implement endpoints which adhere to the [API server specifications](#api-server-specifications).
25 | 3. Create a concise and illustrative README describing how to start your server and where to navigate to view the examples in a browser.
26 | 4. Update the [main README](README.md) and [API README](api/README.md) to link to the code directory of your new example.
27 |
28 | ### API Server specifications
29 |
30 | | Endpoint | Action |
31 | | -------- | ------ |
32 | | POST `/api/subscriptions/new` | New subscriptions |
33 | | POST `/api/accounts/new` | New accounts |
34 | | PUT `/api/accounts/:account_code` | Account updates |
35 |
36 | All other GET requests should serve files directly from the [public directory](public).
37 |
38 | ### External examples
39 |
40 | If you have a site that implements Recurly.js in a novel or cool way, please [create an issue][new-issue]
41 | with a link and we'll link to it in the readme.
42 |
43 | [new-issue]: https://github.com/recurly/recurly-js-examples/issues/new
44 | [new-pr]: https://github.com/recurly/recurly-js-examples/pulls/new
45 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | ./api/ruby/Dockerfile
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | ./api/ruby/Gemfile
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | ./api/ruby/Gemfile.lock
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Recurly, inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: PUBLIC_DIR_PATH=public bundle exec ruby api/ruby/app.rb -p $PORT
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Recurly integration examples
7 | ===================
8 |
9 |
10 |
11 |
12 | This repository contains a set of example implementations of
13 | [recurly.js][recurly-js] using html, css, and JavaScript, and a set of API usage
14 | examples to demonstrate a few common use-cases for integrating with Recurly.
15 |
16 | Please note that these examples are not meant to be set onto a web server and
17 | forgotten. They are intended to act as a suggestion, wherein your custom needs
18 | may fill in any implementaiton gaps.
19 |
20 | ### Payment form examples
21 |
22 | - [Payment form examples][examples]
23 |
24 | ### API usage examples
25 |
26 | - [Ruby](api/ruby)
27 | - [Node](api/node)
28 | - [Python](api/python)
29 | - [PHP](api/php)
30 | - [Java](api/java)
31 | - [C#, ASP.NET Core](api/dotnet)
32 |
33 | #### Configuring the examples
34 |
35 | Each API example will pull configuration values from environment variables. You may set
36 | them to quickly configure the example to connect to your Recurly site.
37 |
38 | | Environment variable | description |
39 | | -------------------- | ----------- |
40 | | RECURLY_SUBDOMAIN | The subdomain of your recurly site |
41 | | RECURLY_API_KEY | Your [private API key][api-keys] |
42 | | RECURLY_PUBLIC_KEY | Your [public API key][api-keys] |
43 | | SUCCESS_URL | A URL to redirect to when an action suceeds |
44 | | ERROR_URL | A URL to redirect to when an error occurs |
45 |
46 | ### How to run
47 |
48 | Each example can be run either locally or through [Docker](https://docs.docker.com/), allowing
49 | you to easily experiment with modifications and get running quickly.
50 |
51 | You should adjust the code to fit your specific redirection and error handling needs, but the
52 | example applications are designed to perform essential API functions on first boot.
53 |
54 | **Note**: These examples are purely for demonstration purposes and we do not recommend using them
55 | as the foundation of a production system.
56 |
57 | #### Docker
58 |
59 | Each example includes a Dockerfile and a docker-compose.yml file to allow them to be run through
60 | [Docker](https://docs.docker.com/).
61 |
62 | To run any of the examples through Docker, clone this repository, update the
63 | [docker.env file at the root of level of the project](docker.env) with values corresponding to your Recurly site, and run `docker-compose up` inside the directory of any of the examples.
64 |
65 | #### Local
66 |
67 | To run locally, simply clone this repository, read through the simple application code to
68 | familiarize yourself, and follow the startup instructions in one of the [API
69 | token usage examples](api) above.
70 |
71 | #### Deploy Immediately to Cloud Services
72 |
73 | This repository can be deployed immediately to Heroku or Google Cloud using the included
74 | [Ruby example](api/ruby). If you choose to do this, feel free to delete the other language
75 | backends from your clone.
76 |
77 | [](https://heroku.com/deploy)
78 |
79 | [](https://deploy.cloud.run)
80 |
81 | [](https://app.netlify.com/start/deploy?repository=https://github.com/recurly/recurly-integration-examples)
82 |
83 | ### Looking for React?
84 |
85 | If you plan to use React on your frontend, check out our [react-recurly][react-recurly-repo] library.
86 | We maintain an example integration of `react-recurly` in the documentation for that library. Be sure
87 | to read through the [documentation][react-recurly-docs] as you explore the [examples][react-recurly-demo].
88 |
89 | ### Contributing
90 |
91 | [See CONTRIBUTING file](CONTRIBUTING.md).
92 |
93 | ### License
94 |
95 | [MIT](license.md)
96 |
97 | [recurly-js]: https://github.com/recurly/recurly-js
98 | [examples]: public
99 | [api-keys]: https://app.recurly.com/go/integrations/api_keys
100 | [react-recurly-repo]: https://github.com/recurly/react-recurly
101 | [react-recurly-docs]: https://recurly.github.io/react-recurly
102 | [react-recurly-demo]: https://recurly.github.io/react-recurly/?path=/docs/introduction-interactive-demo--page
103 |
--------------------------------------------------------------------------------
/api/README.md:
--------------------------------------------------------------------------------
1 | ### API token usage examples
2 |
3 | Here you will find a set of API backend script and application examples to
4 | demonstrate a few common use-cases for collecting and using recurly.js tokens.
5 |
6 | - [Ruby, Sinatra](ruby)
7 | - [Ruby, Sinatra, Heroku](ruby-heroku)
8 | - [Node, Express](node)
9 | - [Python, Flask](python)
10 | - [PHP, Slim](php)
11 | - [Java, Spark](java)
12 | - [C#, ASP.NET Core](dotnet)
13 |
--------------------------------------------------------------------------------
/api/dotnet/.dockerignore:
--------------------------------------------------------------------------------
1 | **/.classpath
2 | **/.dockerignore
3 | **/.env
4 | **/.git
5 | **/.gitignore
6 | **/.project
7 | **/.settings
8 | **/.toolstarget
9 | **/.vs
10 | **/.vscode
11 | **/*.*proj.user
12 | **/*.dbmdl
13 | **/*.jfm
14 | **/azds.yaml
15 | **/bin
16 | **/charts
17 | **/docker-compose*
18 | **/Dockerfile*
19 | **/node_modules
20 | **/npm-debug.log
21 | **/obj
22 | **/secrets.dev.yaml
23 | **/values.dev.yaml
24 | README.md
25 |
--------------------------------------------------------------------------------
/api/dotnet/Controllers/AppController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Microsoft.AspNetCore.Mvc;
6 | using Microsoft.Extensions.Logging;
7 |
8 | using Recurly;
9 | using Recurly.Resources;
10 |
11 | namespace dotnet.Controllers
12 | {
13 | [ApiController]
14 | public class AppController : ControllerBase
15 | {
16 | private readonly Client _client;
17 |
18 | private readonly ILogger _logger;
19 |
20 | public AppController(ILogger logger)
21 | {
22 | _client = new Client(APIKey);
23 | _logger = logger;
24 | }
25 |
26 | // TODO: Bring over advanced item purchasing from ruby example
27 | [HttpPost("api/subscriptions/new")]
28 | public RedirectResult CreatePurchase([FromForm(Name = "recurly-token")] string tokenId, [FromForm(Name = "account-code")] string accountCode, [FromForm(Name = "first-name")] string firstName, [FromForm(Name = "last-name")] string lastName)
29 | {
30 | // If our form specifies an account code, we can use that; otherwise,
31 | // create an account code with a uniq id
32 | accountCode = accountCode ?? Guid.NewGuid().ToString();
33 |
34 | var purchaseReq = new PurchaseCreate()
35 | {
36 | Currency = "USD",
37 | Account = new AccountPurchase()
38 | {
39 | Code = accountCode,
40 | FirstName = firstName,
41 | LastName = lastName,
42 | BillingInfo = new BillingInfoCreate()
43 | {
44 | TokenId = tokenId
45 | }
46 | },
47 | Subscriptions = new List()
48 | {
49 | new SubscriptionPurchase() { PlanCode = "basic" }
50 | }
51 | };
52 |
53 | try
54 | {
55 | InvoiceCollection collection = _client.CreatePurchase(purchaseReq);
56 | _logger.LogInformation($"Created ChargeInvoice with Number: {collection.ChargeInvoice.Number}");
57 | }
58 | catch (Recurly.Errors.Transaction e)
59 | {
60 | /**
61 | * Note: This is not an example of extensive error handling,
62 | * it is scoped to handling the 3DSecure error for simplicity.
63 | * Please ensure you have proper error handling before going to production.
64 | */
65 | TransactionError transactionError = e.Error.TransactionError;
66 |
67 | if (transactionError != null && transactionError.Code == "three_d_secure_action_required") {
68 | string actionTokenId = transactionError.ThreeDSecureActionTokenId;
69 | return Redirect($"/3d-secure/authenticate.html#token_id={tokenId}&action_token_id={actionTokenId}&account_code={accountCode}");
70 | }
71 |
72 | return HandleError(e);
73 | }
74 | catch (Recurly.Errors.ApiError e)
75 | {
76 | return HandleError(e);
77 | }
78 |
79 | return Redirect(SuccessURL);
80 | }
81 |
82 | [HttpPost("api/accounts/new")]
83 | public ActionResult CreateAccount([FromForm(Name = "account-code")] string accountCode, [FromForm(Name = "first-name")] string firstName, [FromForm(Name = "last-name")] string lastName)
84 | {
85 | // If our form specifies an account code, we can use that; otherwise,
86 | // create an account code with a uniq id
87 | accountCode = accountCode ?? Guid.NewGuid().ToString();
88 |
89 | var accountReq = new AccountCreate()
90 | {
91 | Code = accountCode,
92 | FirstName = firstName,
93 | LastName = lastName
94 | };
95 |
96 | try
97 | {
98 | Account account = _client.CreateAccount(accountReq);
99 | _logger.LogInformation($"Created account {account.Code}");
100 | }
101 | catch (Recurly.Errors.ApiError e)
102 | {
103 | return HandleError(e);
104 | }
105 |
106 | return Redirect(SuccessURL);
107 | }
108 |
109 | [HttpPut("api/accounts/{accountId}")]
110 | public ActionResult UpdateAccount(string accountId, [FromForm(Name = "first-name")] string firstName, [FromForm(Name = "last-name")] string lastName)
111 | {
112 | var accountReq = new AccountUpdate() {
113 | FirstName = firstName,
114 | LastName = lastName
115 | };
116 |
117 | try
118 | {
119 | Account account = _client.UpdateAccount(accountId, accountReq);
120 | _logger.LogInformation($"Updated account {account.Code}");
121 | }
122 | catch (Recurly.Errors.ApiError e)
123 | {
124 | return HandleError(e);
125 | }
126 |
127 | return Redirect(SuccessURL);
128 | }
129 |
130 | /* This endpoint provides configuration to recurly.js */
131 | [HttpGet("config")]
132 | public ContentResult GetGonfig()
133 | {
134 | var config = new {
135 | publicKey = APIPublicKey
136 | };
137 |
138 | return Content($"window.recurlyConfig = {System.Text.Json.JsonSerializer.Serialize(config)}", "application/javascript");
139 | }
140 |
141 | private RedirectResult HandleError(Exception e)
142 | {
143 | _logger.LogError(e, "Exception caught: redirecting");
144 | return Redirect($"{ErrorURL}?error={e.Message}");
145 | }
146 |
147 | private string APIKey
148 | {
149 | get { return Environment.GetEnvironmentVariable("RECURLY_API_KEY"); }
150 | }
151 |
152 | private string APIPublicKey
153 | {
154 | get { return Environment.GetEnvironmentVariable("RECURLY_PUBLIC_KEY"); }
155 | }
156 |
157 | private string SuccessURL
158 | {
159 | get {
160 | string url = Environment.GetEnvironmentVariable("SUCCESS_URL");
161 | return String.IsNullOrEmpty(url) ? "/" : url;
162 | }
163 | }
164 |
165 | private string ErrorURL
166 | {
167 | get {
168 | string url = Environment.GetEnvironmentVariable("ERROR_URL");
169 | return String.IsNullOrEmpty(url) ? "/" : url;
170 | }
171 | }
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/api/dotnet/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mcr.microsoft.com/dotnet/aspnet:3.1 AS base
2 | WORKDIR /app
3 | EXPOSE 9001
4 |
5 | FROM mcr.microsoft.com/dotnet/sdk:3.1 AS build
6 | WORKDIR /usr/src/app
7 | COPY ["./api/dotnet/dotnet.csproj", "./api/dotnet/"]
8 | RUN dotnet restore "./api/dotnet/dotnet.csproj"
9 | COPY ./api/dotnet ./api/dotnet
10 | CMD dotnet run --project "./api/dotnet"
11 |
--------------------------------------------------------------------------------
/api/dotnet/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Microsoft.AspNetCore.Hosting;
6 | using Microsoft.Extensions.Configuration;
7 | using Microsoft.Extensions.Hosting;
8 | using Microsoft.Extensions.Logging;
9 |
10 | namespace dotnet
11 | {
12 | public class Program
13 | {
14 | public static void Main(string[] args)
15 | {
16 | CreateHostBuilder(args).Build().Run();
17 | }
18 |
19 | public static IHostBuilder CreateHostBuilder(string[] args) =>
20 | Host.CreateDefaultBuilder(args)
21 | .ConfigureWebHostDefaults(webBuilder =>
22 | {
23 | webBuilder.UseStartup();
24 | });
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/api/dotnet/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/launchsettings.json",
3 | "iisSettings": {
4 | "windowsAuthentication": false,
5 | "anonymousAuthentication": true,
6 | "iisExpress": {
7 | "applicationUrl": "http://localhost:19142",
8 | "sslPort": 0
9 | }
10 | },
11 | "profiles": {
12 | "IIS Express": {
13 | "commandName": "IISExpress",
14 | "launchBrowser": true,
15 | "launchUrl": "",
16 | "environmentVariables": {
17 | "ASPNETCORE_ENVIRONMENT": "Development"
18 | }
19 | },
20 | "dotnet": {
21 | "commandName": "Project",
22 | "launchBrowser": true,
23 | "launchUrl": "",
24 | "applicationUrl": "http://+:9001",
25 | "environmentVariables": {
26 | "ASPNETCORE_ENVIRONMENT": "Development"
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/api/dotnet/README.md:
--------------------------------------------------------------------------------
1 | ## API example: C# + ASP.NET Core
2 |
3 | This small application demonstrates how you might set up a web server
4 | using C# and [ASP.NET Core][aspnet] with RESTful routes to accept your Recurly.js
5 | form submissions and use the tokens to create and update customer billing
6 | information without having to handle credit card data.
7 |
8 | This example makes use of the official Recurly [.NET client library][client]
9 | for API v3.
10 |
11 | Note that it is not necessary to use the ASP.NET framework. In this example it is
12 | used to organize various API actions into distinct application routes, but one
13 | could just as easily implement these API actions in application framework altogether.
14 |
15 | ### Routes
16 |
17 | - `POST` [/api/subscriptions/new](Controllers/AppController.cs#L28-L80)
18 | - `POST` [/api/accounts/new](Controllers/AppController.cs#L83-L107)
19 | - `PUT` [/api/accounts/:account_code](Controllers/AppController.cs#L110-L128)
20 | - `GET` [/config](Controllers/AppController.cs#L132-L139)
21 |
22 | ### Use
23 |
24 | #### Docker
25 |
26 | 1. If you haven't already, [install docker](https://www.docker.com/get-started).
27 |
28 | 1. Update the values in docker.env at the (root of the repo)[https://github.com/recurly/recurly-integration-examples/blob/main/docker.env]
29 |
30 | 1. Run `docker-compose up --build`
31 |
32 | 1. Open [http://localhost:9001](http://localhost:9001)
33 |
34 | #### Local
35 |
36 | 1. Install [.NET Core][dotnet]. These instructions assume the `dotnet` CLI is present and executable.
37 |
38 | 1. [Set your environment variables][env].
39 |
40 | 1. Start the [Kestrel][kestrel] server.
41 |
42 | ```bash
43 | $ dotnet run --project "./api/dotnet"
44 | ```
45 |
46 | [aspnet]: https://docs.microsoft.com/en-us/aspnet/core/introduction-to-aspnet-core?view=aspnetcore-3.1
47 | [client]: https://github.com/recurly/recurly-client-dotnet
48 | [dotnet]: https://dotnet.microsoft.com/download
49 | [env]: https://github.com/recurly/recurly-integration-examples#configuring-the-examples
50 | [kestrel]: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel?view=aspnetcore-3.1
51 |
--------------------------------------------------------------------------------
/api/dotnet/Startup.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Microsoft.AspNetCore.Builder;
6 | using Microsoft.AspNetCore.Hosting;
7 | using Microsoft.AspNetCore.Mvc;
8 | using Microsoft.Extensions.Configuration;
9 | using Microsoft.Extensions.DependencyInjection;
10 | using Microsoft.Extensions.Hosting;
11 | using Microsoft.Extensions.Logging;
12 |
13 | namespace dotnet
14 | {
15 | public class Startup
16 | {
17 | public Startup(IConfiguration configuration)
18 | {
19 | Configuration = configuration;
20 | }
21 |
22 | public IConfiguration Configuration { get; }
23 |
24 | // This method gets called by the runtime. Use this method to add services to the container.
25 | public void ConfigureServices(IServiceCollection services)
26 | {
27 | services.AddControllers();
28 | }
29 |
30 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
31 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
32 | {
33 | if (env.IsDevelopment())
34 | {
35 | app.UseDeveloperExceptionPage();
36 | }
37 |
38 | app.UseDefaultFiles();
39 | app.UseStaticFiles();
40 |
41 | app.UseRouting();
42 |
43 | app.UseAuthorization();
44 |
45 | app.UseEndpoints(endpoints =>
46 | {
47 | endpoints.MapControllers();
48 | });
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/api/dotnet/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/api/dotnet/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | },
9 | "AllowedHosts": "*"
10 | }
11 |
--------------------------------------------------------------------------------
/api/dotnet/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | app:
4 | build:
5 | context: ../..
6 | dockerfile: ./api/dotnet/Dockerfile
7 | volumes:
8 | - .:/usr/src/app/api/dotnet
9 | - ../../public:/usr/src/app/api/dotnet/wwwroot
10 | ports:
11 | - "9001:9001"
12 | env_file:
13 | - ../../docker.env
14 | environment:
15 | - PUBLIC_DIR_PATH=/usr/src/app/api/dotnet/wwwroot
16 | - ASPNETCORE_ENVIRONMENT=Development
17 | - ASPNETCORE_URLS=http://+:9001
18 |
--------------------------------------------------------------------------------
/api/dotnet/dotnet.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp3.1
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/api/go/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:latest
2 |
3 | WORKDIR /usr/src/app
4 |
5 | COPY ./api/go ./go
6 |
7 | COPY ./public ./public
8 |
9 | EXPOSE 9001
10 |
11 | CMD cd go && go run main.go
12 |
--------------------------------------------------------------------------------
/api/go/README.md:
--------------------------------------------------------------------------------
1 | ## API example: Go + Express
2 |
3 | This small application demonstrates how you might set up a web server
4 | using Go with RESTful routes to accept your Recurly.js
5 | form submissions and use the tokens to create and update customer billing
6 | information without having to handle credit card data.
7 |
8 | This example makes use of the official [Go client library][gp-client-library] for API v3.
9 |
10 | ### Routes
11 |
12 | - `POST` [/api/subscriptions/new](main.go#L25-96)
13 | - `POST` [/api/accounts/new](main.go#99-124)
14 | - `PUT` [/api/accounts/:account_code](main.go#127-151)
15 |
16 | ### Use
17 |
18 | #### Docker
19 |
20 | 1. If you haven't already, [install docker](https://www.docker.com/get-started).
21 |
22 | 2. Update the values in docker.env at the (root of the repo)[https://github.com/recurly/recurly-integration-examples/blob/main/docker.env]
23 |
24 | 3. Run `docker-compose up --build`
25 |
26 | 4. Open [http://localhost:9001](http://localhost:9001)
27 |
28 | #### Local
29 |
30 | 1. Start the server
31 |
32 | ```bash
33 | $ go run main.go
34 | ```
35 | 2. Open [http://localhost:9001](http://localhost:9001)
36 |
37 | [client]: https://github.com/recurly/recurly-client-go
38 |
--------------------------------------------------------------------------------
/api/go/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | app:
4 | build:
5 | context: ../..
6 | dockerfile: ./api/go/Dockerfile
7 | volumes:
8 | - .:/usr/src/app/go
9 | - ../../public:/usr/src/app/public
10 | ports:
11 | - "9001:9001"
12 | env_file:
13 | - ../../docker.env
14 | environment:
15 | - PUBLIC_DIR_PATH=/usr/src/app/public
16 |
--------------------------------------------------------------------------------
/api/go/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/recurly/recurly-integration-examples/tree/main/api/go
2 |
3 | go 1.15
4 |
5 | require (
6 | github.com/google/uuid v1.1.2
7 | github.com/recurly/recurly-client-go/v3 v3.8.0
8 | )
9 |
--------------------------------------------------------------------------------
/api/go/go.sum:
--------------------------------------------------------------------------------
1 | github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
2 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
3 | github.com/recurly/recurly-client-go v0.0.0-20200518190456-e30417f663e1 h1:rgArsvQso1sbE8tvBVpa9mKE3vdFqAtzbt9V3AV85z4=
4 | github.com/recurly/recurly-client-go/v3 v3.8.0 h1:khCnexPoqNvYy4kkfvq1hvuyntHndFUvCyecs63NAv0=
5 | github.com/recurly/recurly-client-go/v3 v3.8.0/go.mod h1:4qKAuNK6JbnLwhd7M3ZcD6Jbniq9M1ESBwSxbLaS9eQ=
6 |
--------------------------------------------------------------------------------
/api/go/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | // API usage Dependencies
4 | import (
5 | "fmt"
6 | "github.com/google/uuid"
7 | "github.com/recurly/recurly-client-go/v3"
8 | "net/http"
9 | "os"
10 | "strings"
11 | )
12 |
13 | // These are the various configuration values used in this example. They are
14 | // pulled from the ENV for ease of use, but can be defined directly or stored
15 | // elsewhere
16 | var RECURLY_PUBLIC_KEY = os.Getenv("RECURLY_PUBLIC_KEY")
17 | var RECURLY_API_KEY = os.Getenv("RECURLY_API_KEY")
18 | var SUCCESS_URL = os.Getenv("SUCCESS_URL")
19 | var ERROR_URL = os.Getenv("ERROR_URL")
20 |
21 | // Instantiate a configured recurly client
22 | var client = recurly.NewClient(RECURLY_API_KEY)
23 |
24 | // POST route to handle a new subscription form
25 | func createSubscription(w http.ResponseWriter, r *http.Request) {
26 | if r.Method != "POST" {
27 | http.Error(w, "Invalid request method.", 405)
28 | }
29 |
30 | accountCode := r.FormValue("recurly-account-code")
31 |
32 | // Create an accountCode if one is not sent in the request
33 | if accountCode == "" {
34 | accountCode = uuid.New().String()
35 | }
36 |
37 | tokenId := r.FormValue("recurly-token")
38 |
39 | // Build the billing info body
40 | billingInfo := &recurly.BillingInfoCreate{
41 | TokenId: recurly.String(tokenId),
42 | }
43 |
44 | // Optionally add a 3D Secure token if one is present. You only need to do this
45 | // if you are integrating with Recurly's 3D Secure support
46 | threeDSecureToken := r.FormValue("three-d-secure-token")
47 |
48 | // If the request includes a threeDSecureToken, add this to the BillingInfo body
49 | if threeDSecureToken != "" {
50 | billingInfo = &recurly.BillingInfoCreate{
51 | TokenId: recurly.String(tokenId),
52 | ThreeDSecureActionResultTokenId: recurly.String(threeDSecureToken),
53 | }
54 | }
55 |
56 | // Create the purchase using minimal information: planCode, currency, account.code, and
57 | // the token we generated on the frontend
58 | purchaseCreate := &recurly.PurchaseCreate{
59 | Subscriptions: []recurly.SubscriptionPurchase{{PlanCode: recurly.String("basic")}},
60 | Currency: recurly.String("USD"),
61 | Account: &recurly.AccountPurchase{
62 | Code: recurly.String(accountCode),
63 | BillingInfo: billingInfo,
64 | },
65 | }
66 |
67 | _, err := client.CreatePurchase(purchaseCreate)
68 |
69 | if e, ok := err.(*recurly.Error); ok {
70 | // Handle 3D Secure required error by redirecting to an authentication page
71 | if e.TransactionError.Code == "three_d_secure_action_required" {
72 | baseUrl := "/3d-secure/authenticate.html"
73 |
74 | params := fmt.Sprintf(
75 | "token_id=%s&action_token_id=%s&account_code=%s",
76 | tokenId,
77 | e.TransactionError.ThreeDSecureActionTokenId,
78 | accountCode,
79 | )
80 |
81 | url := fmt.Sprintf("%s#%s", baseUrl, params)
82 |
83 | http.Redirect(w, r, url, 303)
84 | } else {
85 | // If any other error occurs,
86 | // redirect to an error page with the error as a query param
87 | errorUrl := fmt.Sprintf("%s?error=%s", ERROR_URL, e.Message)
88 |
89 | http.Redirect(w, r, errorUrl, 303)
90 | }
91 | } else {
92 | // If no errors occur, redirect to the configured success URL
93 | http.Redirect(w, r, SUCCESS_URL, 303)
94 | }
95 |
96 | }
97 |
98 | // POST route to handle a new account form
99 | func createAccount(w http.ResponseWriter, r *http.Request) {
100 | if r.Method != "POST" {
101 | http.Error(w, "Invalid request method.", 405)
102 | }
103 |
104 | accountCode := uuid.New().String()
105 |
106 | tokenId := r.FormValue("recurly-token")
107 |
108 | accountCreate := &recurly.AccountCreate{
109 | Code: recurly.String(accountCode),
110 | BillingInfo: &recurly.BillingInfoCreate{
111 | TokenId: recurly.String(tokenId),
112 | },
113 | }
114 |
115 | _, err := client.CreateAccount(accountCreate)
116 |
117 | if e, ok := err.(*recurly.Error); ok {
118 | errorUrl := fmt.Sprintf("%s?error=%s", ERROR_URL, e.Message)
119 |
120 | http.Redirect(w, r, errorUrl, 303)
121 | } else {
122 | http.Redirect(w, r, SUCCESS_URL, 303)
123 | }
124 | }
125 |
126 | // PUT route to handle an account update form
127 | func updateAccount(w http.ResponseWriter, r *http.Request) {
128 | if r.Method != "PUT" {
129 | http.Error(w, "Invalid request method.", 405)
130 | }
131 |
132 | tokenId := r.FormValue("recurly-token")
133 |
134 | accountUpdate := &recurly.AccountUpdate{
135 | BillingInfo: &recurly.BillingInfoCreate{
136 | TokenId: recurly.String(tokenId),
137 | },
138 | }
139 |
140 | accountCode := fmt.Sprintf("%s", strings.Split(r.URL.String(), "/")[3])
141 |
142 | _, err := client.UpdateAccount(accountCode, accountUpdate)
143 |
144 | if e, ok := err.(*recurly.Error); ok {
145 | errorUrl := fmt.Sprintf("%s?error=%s", ERROR_URL, e.Message)
146 |
147 | http.Redirect(w, r, errorUrl, 303)
148 | } else {
149 | http.Redirect(w, r, SUCCESS_URL, 303)
150 | }
151 | }
152 |
153 | func config(w http.ResponseWriter, req *http.Request) {
154 | req.Header.Add("Content-Type", "application/javascript")
155 |
156 | response := fmt.Sprintf("window.recurlyConfig = { publicKey: '%s' }", RECURLY_PUBLIC_KEY)
157 |
158 | fmt.Fprintf(w, response)
159 | }
160 |
161 | func main() {
162 | publicDirPath := os.Getenv("PUBLIC_DIR_PATH")
163 |
164 | if publicDirPath == "" {
165 | publicDirPath = "../../public"
166 | }
167 |
168 | http.Handle("/", http.FileServer(http.Dir(publicDirPath)))
169 |
170 | http.HandleFunc("/config", config)
171 |
172 | http.HandleFunc("/api/subscriptions/new", createSubscription)
173 |
174 | http.HandleFunc("/api/accounts/new", createAccount)
175 |
176 | http.HandleFunc("/api/accounts/", updateAccount)
177 |
178 | http.ListenAndServe(":9001", nil)
179 | }
180 |
--------------------------------------------------------------------------------
/api/java/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM maven:3
2 |
3 | WORKDIR /usr/src/app
4 |
5 | COPY ./api/java ./java
6 |
7 | # src/main/resources is the expected directory for serving static files
8 | # For more info, see http://sparkjava.com/documentation#static-files
9 | COPY ./public ./src/main/resources/public
10 |
11 | EXPOSE 9001
12 |
13 | CMD cd java && mvn clean compile && mvn exec:java -Dexec.mainClass="com.recurly.examples.App"
14 |
--------------------------------------------------------------------------------
/api/java/README.md:
--------------------------------------------------------------------------------
1 | ## API example: Java + Spark
2 |
3 | This small application demonstrates how you might set up a web server
4 | using Java and [Spark][spark] with RESTful routes to accept your Recurly.js
5 | form submissions and use the tokens to create and update customer billing
6 | information without having to handle credit card data.
7 |
8 | This example makes use of the un-official Recurly [Java client library][client]
9 | for API v2.
10 |
11 | Note that it is not necessary to use the Spark framework. In this example it is
12 | used to organize various API actions into distinct application routes, but one
13 | could just as easily implement these API actions in separate Java classes or
14 | within another application framework altogether.
15 |
16 | ### Routes
17 |
18 | - `POST` [/api/subscriptions/new](src/main/java/com/recurly/App.java#L33-L92)
19 | - `POST` [/api/accounts/new](src/main/java/com/recurly/App.java#L95-L110)
20 | - `PUT` [/api/accounts/:account_code](src/main/java/com/recurly/App.java#L113-L126)
21 |
22 | ### Use
23 |
24 | #### Docker
25 |
26 | 1. If you haven't already, [install docker](https://www.docker.com/get-started).
27 |
28 | 2. Update the values in docker.env at the (root of the repo)[https://github.com/recurly/recurly-integration-examples/blob/main/docker.env]
29 |
30 | 3. Run `docker-compose up --build`
31 |
32 | 4. Open [http://localhost:9001](http://localhost:9001)
33 |
34 | #### Local
35 |
36 | 1. Install dependencies
37 |
38 | The Recurly Java library is distributed via [Maven Central](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.ning.billing%22%20AND%20a%3A%22recurly-java-library%22):
39 |
40 | ```xml
41 |
42 | com.ning.billing
43 | recurly-java-library
44 | 0.29.0
45 |
46 | ```
47 |
48 | The Spark Web Framework is distributed via [Maven Central](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.sparkjava%22%20a%3A%22spark-core%22)
49 |
50 | ```xml
51 |
52 | com.sparkjava
53 | spark-core
54 | 2.9.1
55 |
56 | ```
57 |
58 | 2. Start the server
59 |
60 | ```bash
61 | $ mvn clean compile && mvn exec:java -Dexec.mainClass="com.recurly.examples.App"
62 | ```
63 |
64 | 3. Open [http://localhost:9001](http://localhost:9001)
65 |
66 |
67 | [spark]: http://sparkjava.com/
68 | [client]: https://github.com/killbilling/recurly-java-library
69 |
--------------------------------------------------------------------------------
/api/java/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | app:
4 | build:
5 | context: ../..
6 | dockerfile: ./api/java/Dockerfile
7 | volumes:
8 | - .:/usr/src/app/java
9 | - ../../public:/usr/src/app/src/main/resources/public
10 | ports:
11 | - "9001:9001"
12 | env_file:
13 | - ../../docker.env
14 | environment:
15 | - PUBLIC_DIR_PATH=/usr/src/app/src/main/resources/public
16 |
--------------------------------------------------------------------------------
/api/java/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | com.recurly.examples
8 | recurly-js-examples
9 | 1.0-SNAPSHOT
10 |
11 |
12 | 1.8
13 | 1.8
14 | UTF-8
15 |
16 |
17 |
18 |
19 | com.ning.billing
20 | recurly-java-library
21 | 0.29.0
22 |
23 |
24 | com.sparkjava
25 | spark-core
26 | 2.9.1
27 |
28 |
29 | org.codehaus.mojo
30 | exec-maven-plugin
31 | 1.6.0
32 |
33 |
34 | org.apache.maven
35 | maven-core
36 |
37 |
38 |
39 |
40 | org.slf4j
41 | slf4j-simple
42 | 1.7.10
43 |
44 |
45 | javax.xml.bind
46 | jaxb-api
47 | 2.3.0
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/api/java/src/main/java/com/recurly/App.java:
--------------------------------------------------------------------------------
1 | /* Import Spark and Recurly client library */
2 | package com.recurly.examples;
3 |
4 | import static spark.Spark.*;
5 |
6 | import com.ning.billing.recurly.RecurlyClient;
7 | import com.ning.billing.recurly.model.Account;
8 | import com.ning.billing.recurly.model.BillingInfo;
9 | import com.ning.billing.recurly.model.Subscription;
10 | import com.ning.billing.recurly.TransactionErrorException;
11 | import com.ning.billing.recurly.RecurlyAPIException;
12 | import com.ning.billing.recurly.model.RecurlyErrors;
13 | import com.ning.billing.recurly.model.TransactionError;
14 |
15 | /* We'll use UUID to generate unique account codes */
16 | import java.util.UUID;
17 |
18 | public class App {
19 | @SuppressWarnings("deprecation")
20 | public static void main(String[] args) {
21 | /* Configure the Recurly client with your API key */
22 | String apiKey = System.getenv("RECURLY_API_KEY");
23 | String subdomain = System.getenv("RECURLY_SUBDOMAIN");
24 | String publicKey = System.getenv("RECURLY_PUBLIC_KEY");
25 | String successUrl = System.getenv("SUCCESS_URL");
26 | String errorUrl = System.getenv("ERROR_URL");
27 | String publicDir = System.getenv("PUBLIC_DIR_PATH");
28 |
29 | RecurlyClient recurlyClient = new RecurlyClient(apiKey, subdomain);
30 | setPort(9001);
31 | if (publicDir != null) {
32 | staticFiles.externalLocation(publicDir);
33 | } else {
34 | externalStaticFileLocation("../../public");
35 | }
36 |
37 | /* POST route to handle a new subscription form */
38 | post("/api/subscriptions/new", (req, res) -> {
39 | /* Create the subscription using minimal
40 | information: plan_code, account_code, currency and
41 | the token we generated on the front-end */
42 | String tokenId = req.queryParams("recurly-token");
43 |
44 | Subscription subscriptionData = new Subscription();
45 | subscriptionData.setPlanCode("basic");
46 | subscriptionData.setCurrency("USD");
47 |
48 | Account accountData = new Account();
49 | String accountCode = req.queryParams("recurly-account-code");
50 | if (accountCode == null) {
51 | accountCode = UUID.randomUUID().toString();
52 | }
53 | accountData.setAccountCode(accountCode);
54 |
55 | BillingInfo billingInfoData = new BillingInfo();
56 | billingInfoData.setTokenId(tokenId);
57 |
58 | /* If it exists, set the 3D Secure Action Result Token returned from Recurly.js */
59 | String threeDSART = req.queryParams("three-d-secure-token");
60 | if (threeDSART != null && !threeDSART.isEmpty()) {
61 | billingInfoData.setThreeDSecureActionResultTokenId(threeDSART);
62 | }
63 | accountData.setBillingInfo(billingInfoData);
64 |
65 | subscriptionData.setAccount(accountData);
66 |
67 | recurlyClient.open();
68 |
69 | try {
70 | recurlyClient.createSubscription(subscriptionData);
71 |
72 | /*The subscription has been created and we can redirect
73 | to a confirmation page */
74 | res.redirect(successUrl);
75 | } catch(TransactionErrorException e) {
76 | /**
77 | * Note: This is not an example of extensive error handling,
78 | * it is scoped to handling the 3DSecure error for simplicity.
79 | * Please ensure you have proper error handling before going to production.
80 | */
81 | TransactionError transactionError = e.getErrors().getTransactionError();
82 |
83 | if (transactionError != null) {
84 | String actionTokenId = transactionError.getThreeDSecureActionTokenId();
85 | res.redirect("/3d-secure/authenticate.html#token_id=" + tokenId + "&action_token_id=" + actionTokenId + "&account_code=" + accountCode);
86 | }
87 | }
88 |
89 | recurlyClient.close();
90 | return res.body();
91 | });
92 |
93 | /* POST route to handle a new account form */
94 | post("/api/accounts/new", (req, res) -> {
95 | Account accountData = new Account();
96 | accountData.setAccountCode(UUID.randomUUID().toString());
97 |
98 | BillingInfo billingInfoData = new BillingInfo();
99 | billingInfoData.setTokenId(req.queryParams("recurly-token"));
100 |
101 | accountData.setBillingInfo(billingInfoData);
102 |
103 | recurlyClient.open();
104 | recurlyClient.createAccount(accountData);
105 | recurlyClient.close();
106 |
107 | res.redirect(successUrl);
108 | return res;
109 | });
110 |
111 | /* PUT route to handle an account update form */
112 | put("/api/accounts/:account_code", (req, res) -> {
113 | BillingInfo billingInfoData = new BillingInfo();
114 | billingInfoData.setTokenId(req.queryParams("recurly-token"));
115 |
116 | Account accountData = new Account();
117 | accountData.setBillingInfo(billingInfoData);
118 |
119 | recurlyClient.open();
120 | recurlyClient.updateAccount(req.params(":account_code"), accountData);
121 | recurlyClient.close();
122 |
123 | res.redirect(successUrl);
124 | return res;
125 | });
126 |
127 |
128 | /* This endpoint provides configuration to recurly.js */
129 | get("/config", (req, res) -> {
130 | res.type("application/javascript");
131 | res.body("window.recurlyConfig = { publicKey: '"+ publicKey + "' }");
132 | return res.body();
133 | });
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/api/netlify/functions/config.js:
--------------------------------------------------------------------------------
1 | exports.handler = async (event, context) => {
2 | return {
3 | statusCode: 200,
4 | body: `window.recurlyConfig = { publicKey: '${process.env.RECURLY_PUBLIC_KEY}' }`,
5 | headers: {
6 | 'Content-Type': 'application/javascript'
7 | }
8 | };
9 | };
10 |
--------------------------------------------------------------------------------
/api/netlify/functions/new-account.js:
--------------------------------------------------------------------------------
1 | const recurly = require('recurly');
2 | const uuid = require('node-uuid');
3 |
4 | const client = new recurly.Client(process.env.RECURLY_API_KEY);
5 |
6 | exports.handler = async (event, context) => {
7 | const body = JSON.parse(event.body);
8 |
9 | try {
10 | const accountCreate = {
11 | code: uuid.v1(),
12 | billing_info: {
13 | token_id: body['recurly-token']
14 | }
15 | }
16 |
17 | await client.createAccount(accountCreate);
18 |
19 | return {
20 | statusCode: 301,
21 | headers: {
22 | Location: process.env.SUCCESS_URL
23 | }
24 | };
25 | }
26 | catch (err) {
27 | const { message } = err;
28 |
29 | return {
30 | statusCode: 301,
31 | headers: {
32 | Location: `${process.env.ERROR_URL}?error=${message}`
33 | }
34 | };
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/api/netlify/functions/new-subscription.js:
--------------------------------------------------------------------------------
1 | const recurly = require('recurly');
2 | const uuid = require('node-uuid');
3 | const querystring = require('querystring');
4 |
5 | const client = new recurly.Client(process.env.RECURLY_API_KEY);
6 |
7 | exports.handler = async (event, context) => {
8 | const body = querystring.parse(event.body);
9 |
10 | // Build our billing info hash
11 | const tokenId = body['recurly-token'];
12 | const code = body['recurly-account-code'] || uuid.v1();
13 | const billingInfo = { token_id: tokenId };
14 |
15 | // Optionally add a 3D Secure token if one is present. You only need to do this
16 | // if you are integrating with Recurly's 3D Secure support
17 | if (body['three-d-secure-token']) {
18 | billingInfo.three_d_secure_action_result_token_id = body['three-d-secure-token'];
19 | }
20 |
21 | // Create the purchase using minimal
22 | // information: planCode, currency, account.code, and
23 | // the token we generated on the frontend
24 | const purchaseReq = {
25 | subscriptions: [{ planCode: 'basic' }],
26 | currency: 'USD',
27 | account: { code, billingInfo }
28 | };
29 |
30 | try {
31 | await client.createPurchase(purchaseReq);
32 |
33 | return {
34 | statusCode: 301,
35 | headers: {
36 | Location: process.env.SUCCESS_URL
37 | }
38 | };
39 | } catch (err) {
40 | // Here we handle a 3D Secure required error by redirecting to an authentication page
41 | if (err && err.transactionError && err.transactionError.code === 'three_d_secure_action_required') {
42 | const { threeDSecureActionTokenId } = err.transactionError;
43 | const url = `/3d-secure/authenticate.html#token_id=${tokenId}&action_token_id=${threeDSecureActionTokenId}&account_code=${code}`;
44 |
45 | return {
46 | statusCode: 301,
47 | headers: {
48 | Location: url
49 | }
50 | };
51 | }
52 |
53 | // If any other error occurs, redirect to an error page with the error as a query param
54 | const { message } = err;
55 |
56 | return {
57 | statusCode: 301,
58 | headers: {
59 | Location: `${process.env.ERROR_URL}?error=${message}`
60 | }
61 | };
62 | }
63 | };
64 |
--------------------------------------------------------------------------------
/api/netlify/functions/update-account.js:
--------------------------------------------------------------------------------
1 | const recurly = require('recurly');
2 |
3 | const client = new recurly.Client(process.env.RECURLY_API_KEY);
4 |
5 | exports.handler = async (event, context) => {
6 | const body = JSON.parse(event.body);
7 | const account = event.path.split('/')[3];
8 |
9 | try {
10 | const accountUpdate = {
11 | billing_info: {
12 | token_id: body['recurly-token']
13 | }
14 | };
15 |
16 | await client.updateAccount(account, accountUpdate);
17 |
18 | return {
19 | statusCode: 301,
20 | headers: {
21 | Location: process.env.SUCCESS_URL
22 | }
23 | };
24 | } catch (err) {
25 | const { message } = err;
26 |
27 | return {
28 | statusCode: 301,
29 | headers: {
30 | Location: `${process.env.ERROR_URL}?error=${message}`
31 | }
32 | };
33 | }
34 | };
35 |
--------------------------------------------------------------------------------
/api/netlify/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "recurly-netlify-example",
3 | "version": "1.0.0",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "node-uuid": {
8 | "version": "1.4.8",
9 | "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz",
10 | "integrity": "sha1-sEDrCSOWivq/jTL7HxfxFn/auQc="
11 | },
12 | "recurly": {
13 | "version": "3.14.1",
14 | "resolved": "https://registry.npmjs.org/recurly/-/recurly-3.14.1.tgz",
15 | "integrity": "sha512-0HNZpgA7tls3t632UygVVcheoM/iwJe+16cNeAPda5yvrF8Ke2KqYf0AjA5jh0DyWRNbO50wJY66zAHpeq1Ukw=="
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/api/netlify/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "recurly-netlify-example",
3 | "version": "1.0.0",
4 | "description": "An integration example of Recurly with Netlify'",
5 | "scripts": {
6 | "test": "echo \"Error: no test specified\" && exit 1"
7 | },
8 | "repository": {
9 | "type": "git",
10 | "url": "git+https://github.com/recurly/recurly-js-examples.git"
11 | },
12 | "author": "Dave Brudner",
13 | "license": "ISC",
14 | "bugs": {
15 | "url": "https://github.com/recurly/recurly-js-examples/issues"
16 | },
17 | "homepage": "https://github.com/recurly/recurly-js-examples#readme",
18 | "dependencies": {
19 | "node-uuid": "^1.4.1",
20 | "recurly": "^3.14.1"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/api/node/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:12
2 |
3 | WORKDIR /usr/src/app
4 |
5 | COPY ./api/node ./node
6 | COPY ./public ./public
7 |
8 | EXPOSE 9001
9 |
10 | CMD cd node && npm install && node app.js
11 |
--------------------------------------------------------------------------------
/api/node/README.md:
--------------------------------------------------------------------------------
1 | ## API example: Node + Express
2 |
3 | This small application demonstrates how you might set up a web server
4 | using Node.js and [Express][express] with RESTful routes to accept your Recurly.js
5 | form submissions and use the tokens to create and update customer billing
6 | information without having to handle credit card data.
7 |
8 | This example makes use of the official [node client library][node-client-library] for API v3.
9 |
10 | Note that it is not necessary to use the Express framework. In this example it is
11 | used to organize various API actions into distinct application routes, but one
12 | could just as easily implement these API actions within another application
13 | framework.
14 |
15 | ### Routes
16 |
17 | - `POST` [/api/subscriptions/new](app.js#L28-66)
18 | - `POST` [/api/accounts/new](app.js#L69-85)
19 | - `PUT` [/api/accounts/:account_code](app.js#L88-103)
20 |
21 | ### Use
22 |
23 | #### Docker
24 |
25 | 1. If you haven't already, [install docker](https://www.docker.com/get-started).
26 |
27 | 2. Update the values in docker.env at the (root of the repo)[https://github.com/recurly/recurly-integration-examples/blob/main/docker.env]
28 |
29 | 3. Run `docker-compose up --build`
30 |
31 | 4. Open [http://localhost:9001](http://localhost:9001)
32 |
33 | #### Local
34 |
35 | 1. Start the server
36 |
37 | ```bash
38 | $ npm i
39 | $ node app
40 | ```
41 | 2. Open [http://localhost:9001](http://localhost:9001)
42 |
43 | [express]: https://expressjs.com/
44 | [client]: https://github.com/recurly/recurly-client-node
45 |
--------------------------------------------------------------------------------
/api/node/app.js:
--------------------------------------------------------------------------------
1 | // API usage Dependencies
2 | const recurly = require('recurly')
3 | const express = require('express');
4 | const bodyParser = require('body-parser');
5 |
6 | // We'll use uuids to generate account_code values
7 | const uuid = require('node-uuid');
8 |
9 | // Set up express
10 | const app = express();
11 | app.use(bodyParser());
12 |
13 | // These are the various configuration values used in this example. They are
14 | // pulled from the ENV for ease of use, but can be defined directly or stored
15 | // elsewhere
16 | const {
17 | RECURLY_SUBDOMAIN,
18 | RECURLY_API_KEY,
19 | RECURLY_PUBLIC_KEY,
20 | SUCCESS_URL,
21 | ERROR_URL,
22 | PUBLIC_DIR_PATH
23 | } = process.env;
24 |
25 | // Instantiate a configured recurly client
26 | const client = new recurly.Client(RECURLY_API_KEY)
27 |
28 | // POST route to handle a new subscription form
29 | app.post('/api/subscriptions/new', async function (req, res) {
30 | // Build our billing info hash
31 | const tokenId = req.body['recurly-token'];
32 | const code = req.body['recurly-account-code'] || uuid.v1();
33 | const billingInfo = { token_id: tokenId };
34 |
35 | // Optionally add a 3D Secure token if one is present. You only need to do this
36 | // if you are integrating with Recurly's 3D Secure support
37 | if (req.body['three-d-secure-token']) {
38 | billingInfo.three_d_secure_action_result_token_id = req.body['three-d-secure-token']
39 | }
40 |
41 | // Create the purchase using minimal
42 | // information: planCode, currency, account.code, and
43 | // the token we generated on the frontend
44 | const purchaseReq = {
45 | subscriptions: [{ planCode: 'basic' }],
46 | currency: 'USD',
47 | account: { code, billingInfo }
48 | }
49 |
50 | try {
51 | await client.createPurchase(purchaseReq);
52 | res.redirect(SUCCESS_URL);
53 | }
54 | catch (err) {
55 | // Here we handle a 3D Secure required error by redirecting to an authentication page
56 | if (err && err.transactionError && err.transactionError.code === 'three_d_secure_action_required') {
57 | const { threeDSecureActionTokenId } = err.transactionError;
58 | const url = `/3d-secure/authenticate.html#token_id=${tokenId}&action_token_id=${threeDSecureActionTokenId}&account_code=${code}`
59 |
60 | return res.redirect(url);
61 | }
62 |
63 | // If any other error occurs, redirect to an error page with the error as a query param
64 | const { message } = err;
65 | return res.redirect(`${ERROR_URL}?error=${message}`);
66 | }
67 | });
68 |
69 | // POST route to handle a new account form
70 | app.post('/api/accounts/new', async function (req, res) {
71 | try {
72 | const accountCreate = {
73 | code: uuid.v1(),
74 | billing_info: {
75 | token_id: req.body['recurly-token']
76 | }
77 | }
78 |
79 | await client.createAccount(accountCreate);
80 | res.redirect(SUCCESS_URL);
81 | }
82 | catch (err) {
83 | const { message } = err;
84 | return res.redirect(`${ERROR_URL}?error=${message}`);
85 | }
86 | });
87 |
88 | // PUT route to handle an account update form
89 | app.put('/api/accounts/:account_code', async function (req, res) {
90 | try {
91 | const accountUpdate = {
92 | billing_info: {
93 | token_id: req.body['recurly-token']
94 | }
95 | }
96 |
97 | await client.updateAccount(req.params.account_code, accountUpdate);
98 | res.redirect(SUCCESS_URL);
99 | }
100 | catch (err) {
101 | const { message } = err;
102 | return res.redirect(`${ERROR_URL}?error=${message}`);
103 | }
104 | });
105 |
106 | // This endpoint provides configuration to recurly.js
107 | app.get('/config', function (req, res) {
108 | res.setHeader('Content-Type', 'application/javascript');
109 | res.send(`window.recurlyConfig = { publicKey: '${RECURLY_PUBLIC_KEY}' }`);
110 | });
111 |
112 | // Mounts express.static to render example forms
113 | const pubDirPath = PUBLIC_DIR_PATH || '/../../public';
114 |
115 | app.use(express.static(pubDirPath));
116 |
117 | // Start the server
118 | app.listen(9001, function () {
119 | console.log('Listening on port 9001');
120 | });
121 |
--------------------------------------------------------------------------------
/api/node/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 | services:
3 | app:
4 | build:
5 | context: ../..
6 | dockerfile: ./api/node/Dockerfile
7 | volumes:
8 | - .:/usr/src/app/node
9 | - ../../public:/usr/src/app/public
10 | ports:
11 | - "9001:9001"
12 | env_file:
13 | - ../../docker.env
14 | environment:
15 | - PUBLIC_DIR_PATH=/usr/src/app/public
16 |
--------------------------------------------------------------------------------
/api/node/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "recurly-api-example",
3 | "repository": {
4 | "type": "git",
5 | "url": "git://github.com/recurly/recurly-integration-examples.git"
6 | },
7 | "version": "0.0.0",
8 | "description": "A simple web server to make recurly API requests",
9 | "main": "app.js",
10 | "dependencies": {
11 | "body-parser": "^1.2.2",
12 | "express": "^4.3.2",
13 | "node-uuid": "^1.4.1",
14 | "recurly": "^3.13.0"
15 | },
16 | "license": "MIT"
17 | }
18 |
--------------------------------------------------------------------------------
/api/php/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM composer:2
2 |
3 | WORKDIR /usr/src/app
4 |
5 | COPY ./api/php .
6 |
7 | RUN composer install
8 |
9 | COPY ./public ./public
10 |
11 | EXPOSE 9001
12 |
13 | CMD php -S 0.0.0.0:9001 -t ./public app.php
14 |
--------------------------------------------------------------------------------
/api/php/README.md:
--------------------------------------------------------------------------------
1 | ## API example: PHP + Slim
2 |
3 | This small application demonstrates how you might set up a web server
4 | using PHP and [Slim][slim] with RESTful routes to accept your Recurly.js
5 | form submissions and use the tokens to create and update customer billing
6 | information without having to handle credit card data.
7 |
8 | This example makes use of the official Recurly [PHP client library][client]
9 | for API v3.
10 |
11 | Note that it is not necessary to use the Slim framework. In this example it is
12 | used to organize various API actions into distinct application routes, but one
13 | could just as easily implement these API actions in separate PHP scripts or
14 | within another application framework altogehter.
15 |
16 | ### Routes
17 |
18 | - `POST` [/api/subscriptions/new](app.php#L23-L98)
19 | - `POST` [/api/accounts/new](app.php#L100-127)
20 | - `PUT` [/api/accounts/:account_code](app.php#L129-156)
21 | - `GET` [/config](app.php#L158-162)
22 |
23 | ### Use
24 |
25 | #### Docker
26 |
27 | 1. If you haven't already, [install docker](https://www.docker.com/get-started).
28 |
29 | 2. Update the values in docker.env at the (root of the repo)[https://github.com/recurly/recurly-integration-examples/blob/main/docker.env]
30 |
31 | 3. Run `docker-compose up --build`
32 |
33 | 4. Open [http://localhost:9001](http://localhost:9001)
34 |
35 | #### Local
36 |
37 | 1. Install dependencies using [Composer][composer]. These instructions assume a global composer
38 | executable.
39 |
40 | ```bash
41 | $ composer install
42 | ```
43 | 2. [Set your environment variables][env].
44 | 2. Run PHP's built-in server.
45 |
46 | ```bash
47 | $ php -S localhost:9001 -t ../../public app.php
48 | ```
49 |
50 | [slim]: https://www.slimframework.com
51 | [client]: https://github.com/recurly/recurly-client-php
52 | [composer]: https://getcomposer.org
53 | [env]: ../../#configuring-the-examples
54 |
--------------------------------------------------------------------------------
/api/php/app.php:
--------------------------------------------------------------------------------
1 | post('/api/subscriptions/new', function (Request $request, Response $response, array $args) {
27 |
28 | global $recurly_client;
29 |
30 | // Retrieve the parsed body from the request as params
31 | $params = (array)$request->getParsedBody();
32 |
33 | // Retrieve the token created by Recurly.js and submitted in our form
34 | $token_id = $params['recurly-token'];
35 |
36 | // If our form specifies an account code, we can use that; otherwise,
37 | // create an account code with a uniqid
38 | $account_code = $params['recurly-account-code'];
39 | if (is_null($account_code)) {
40 | $account_code = uniqid();
41 | }
42 |
43 | // Specify the minimum purchase attributes for a subscription: plan_code, account, and currency
44 | $purchase_create = [
45 | 'currency' => 'USD',
46 | 'account' => [
47 | 'code' => $account_code,
48 | 'first_name' => $params['first-name'],
49 | 'last_name' => $params['last-name'],
50 | 'billing_info' => [
51 | 'token_id' => $token_id
52 | ],
53 | ],
54 | 'subscriptions' => [
55 | [
56 | 'plan_code' => 'basic'
57 | ]
58 | ]
59 | ];
60 |
61 | // Optionally add a 3D Secure token if one is present
62 | $three_d_secure_token = $params['three-d-secure-token'];
63 | if ($three_d_secure_token) {
64 | $purchase_create['account']['billing_info']['three_d_secure_action_result_token_id'] = $three_d_secure_token;
65 | }
66 |
67 | // We wrap this is a try-catch to handle any errors
68 | try {
69 |
70 | // Create the purchase
71 | $recurly_client->createPurchase($purchase_create);
72 |
73 | } catch (\Recurly\Errors\Transaction $e) {
74 |
75 | // Here we handle a 3D Secure required error by redirecting to an authentication page
76 | $transaction_error = $e->getApiError()->getTransactionError();
77 | if ($transaction_error && $transaction_error->getCode() == 'three_d_secure_action_required') {
78 | $action_token_id = $transaction_error->getThreeDSecureActionTokenId();
79 | $location = "/3d-secure/authenticate.html#token_id=$token_id&action_token_id=$action_token_id&account_code=$account_code";
80 | return $response->withHeader('Location', $location)->withStatus(302);
81 | }
82 |
83 | // Assign the error message and use it to handle any customer messages or logging
84 | $error = $e->getMessage();
85 |
86 | } catch (\Recurly\Errors\Validation $e) {
87 |
88 | // If the request was not valid, you may want to tell your user why.
89 | $error = $e->getMessage();
90 |
91 | }
92 |
93 | // Now we may wish to redirect to a confirmation or back to the form to fix any errors.
94 | $location = $_ENV['SUCCESS_URL'];
95 | if (isset($error)) {
96 | $location = "$_ENV[ERROR_URL]?error=$error";
97 | }
98 |
99 | return $response->withHeader('Location', $location)->withStatus(302);
100 | });
101 |
102 | // Create a new account and billing information
103 | $app->post('/api/accounts/new', function (Request $request, Response $response, array $args) {
104 | global $recurly_client;
105 |
106 | $params = (array)$request->getParsedBody();
107 |
108 | $account_create = [
109 | 'code' => $account_code,
110 | 'first_name' => $params['first-name'],
111 | 'last_name' => $params['last-name'],
112 | 'billing_info' => [
113 | 'token_id' => $params['recurly-token']
114 | ]
115 | ];
116 |
117 | try {
118 | $recurly_client->createAccount($account_create);
119 | } catch (\Recurly\Errors\Validation $e) {
120 | $error = $e->getMessage();
121 | }
122 |
123 | $location = $_ENV['SUCCESS_URL'];
124 | if (isset($error)) {
125 | $location = "$_ENV[ERROR_URL]?error=$error";
126 | }
127 |
128 | return $response->withHeader('Location', $location)->withStatus(302);
129 | });
130 |
131 | $app->put('/api/accounts/{account_code}', function (Request $request, Response $response, array $args) {
132 | global $recurly_client;
133 |
134 | $params = (array)$request->getParsedBody();
135 |
136 | $account_update = [
137 | 'first_name' => $params['first-name'],
138 | 'last_name' => $params['last-name'],
139 | 'billing_info' => [
140 | 'token_id' => $params['recurly-token']
141 | ]
142 | ];
143 |
144 | try {
145 | $recurly_client->updateAccount("code-$args[account_code]", $account_update);
146 | } catch (\Recurly\Errors\Validation $e) {
147 | $error = $e->getMessage();
148 | }
149 |
150 | $location = $_ENV['SUCCESS_URL'];
151 | if (isset($error)) {
152 | $location = "$_ENV[ERROR_URL]?error=$error";
153 | }
154 |
155 | return $response->withHeader('Location', $location)->withStatus(302);
156 | });
157 |
158 | // This endpoint provides configuration to recurly.js
159 | $app->get('/config', function (Request $request, Response $response, array $args) {
160 | $PUBLIC_KEY = getenv('RECURLY_PUBLIC_KEY');
161 | $response->getBody()->write("window.recurlyConfig = { publicKey: '$PUBLIC_KEY' }");
162 | return $response->withHeader('Content-Type', 'application/javascript');
163 | });
164 |
165 | $app->run();
166 |
--------------------------------------------------------------------------------
/api/php/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "require": {
3 | "slim/slim": "^4",
4 | "recurly/recurly-client": "^3",
5 | "slim/psr7": "^1.2"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/api/php/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | app:
4 | build:
5 | context: ../..
6 | dockerfile: ./api/php/Dockerfile
7 | volumes:
8 | - .:/usr/src/app/php
9 | - ../../public:/usr/src/app/public
10 | ports:
11 | - "9001:9001"
12 | env_file:
13 | - ../../docker.env
14 | environment:
15 | - PUBLIC_DIR_PATH=/public
16 |
--------------------------------------------------------------------------------
/api/python/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3
2 |
3 | WORKDIR /usr/src/app
4 |
5 | COPY ./api/python ./python
6 |
7 | RUN cd python && pip install -r requirements.txt
8 |
9 | COPY ./public ./public
10 |
11 | EXPOSE 9001
12 |
13 | CMD FLASK_APP=python/app.py flask run -p 9001 --host 0.0.0.0
14 |
--------------------------------------------------------------------------------
/api/python/README.md:
--------------------------------------------------------------------------------
1 | ## API example: Python + Flask
2 |
3 | This small application demonstrates how you might set up a web server
4 | using Python and [Flask][flask] with RESTful routes to accept your Recurly.js
5 | form submissions and use the tokens to create and update customer billing
6 | information without having to handle credit card data.
7 |
8 | This example makes use of the official Recurly [Python client library][client]
9 | for API v3.
10 |
11 | Note that it is not necessary to use the Flask framework. In this example it is
12 | used to organize various API actions into distinct application routes, but one
13 | could just as easily implement these API actions within another application
14 | framework.
15 |
16 | ### Routes
17 |
18 | - `POST` [/api/subscriptions/new](app.py#L29-L68)
19 | - `POST` [/api/accounts/new](app.py#L84-L103)
20 | - `PUT` [/api/accounts/:account_code](app.py#L106-L118)
21 |
22 | ### Use
23 |
24 | #### Docker
25 |
26 | 1. If you haven't already, [install docker](https://www.docker.com/get-started).
27 |
28 | 2. Update the values in docker.env at the (root of the repo)[https://github.com/recurly/recurly-integration-examples/blob/main/docker.env]
29 |
30 | 3. Run `docker-compose up --build`
31 |
32 | 4. Open [http://localhost:9001](http://localhost:9001)
33 |
34 | #### Local
35 |
36 | 1. Start the server
37 |
38 | ```bash
39 | $ pip install -r requirements.txt
40 | $ FLASK_APP=app.py flask run -p 9001
41 | ```
42 | 2. Open [http://localhost:9001/index.html](http://localhost:9001/index.html)
43 |
44 | [flask]: http://flask.pocoo.org/
45 | [client]: http://github.com/recurly/recurly-client-python
46 |
--------------------------------------------------------------------------------
/api/python/app.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | # Import Flask and recurly client library
4 | from flask import Flask
5 | from flask import request
6 | from flask import Response
7 | from flask import redirect
8 | import recurly
9 |
10 | # We'll use uuid to generate unique account codes
11 | import uuid
12 |
13 | # Configure the recurly client with your api key
14 | client = recurly.Client(os.environ['RECURLY_API_KEY'])
15 |
16 | # Set your Recurly public key
17 | RECURLY_PUBLIC_KEY = os.environ['RECURLY_PUBLIC_KEY']
18 |
19 | SUCCESS_URL = os.environ['SUCCESS_URL']
20 | ERROR_URL = os.environ['ERROR_URL']
21 |
22 | PUBLIC_DIR_PATH = os.getenv('PUBLIC_DIR_PATH', '../../public')
23 | app = Flask(__name__, static_folder=PUBLIC_DIR_PATH, static_url_path='')
24 |
25 | # GET route to show the list of options
26 | @app.route("/", methods=['GET'])
27 | def index():
28 | return redirect('index.html')
29 |
30 | # POST route to handle a new subscription form
31 | @app.route("/api/subscriptions/new", methods=['POST'])
32 | def new_purchase():
33 | # We'll wrap this in a try to catch any API
34 | # errors that may occur
35 | try:
36 | recurly_token_id = request.form['recurly-token']
37 | # Access or generate account_code
38 | if 'recurly-account-code' in request.form:
39 | recurly_account_code = request.form['recurly-account-code']
40 | else:
41 | recurly_account_code = str(uuid.uuid1())
42 | billing_info = { "token_id": recurly_token_id }
43 |
44 | # Optionally add a 3D Secure token if one is present. You only need to do this
45 | # if you are integrating with Recurly's 3D Secure support
46 | if 'three-d-secure-token' in request.form:
47 | billing_info['three_d_secure_action_result_token_id'] = request.form['three-d-secure-token']
48 |
49 | # Create the subscription using minimal
50 | # information: plan_code, account_code, currency and
51 | # the token we generated on the frontend
52 | # For this we will use the Recurly Create Purchase endpoint
53 | # See: https://developers.recurly.com/api/latest/index.html#operation/create_purchase
54 | purchase_create = {
55 | "currency": "USD",
56 | "account": {
57 | "code": recurly_account_code,
58 | "billing_info": billing_info,
59 | },
60 | "subscriptions": [{"plan_code": "basic"}],
61 | }
62 | invoice_collection = client.create_purchase(purchase_create)
63 |
64 | # The purchase has been created and we can redirect
65 | # to a confirmation page
66 |
67 | return redirect(SUCCESS_URL)
68 | except recurly.errors.TransactionError as error:
69 | transaction_error = error.error.get_response().body['transaction_error']
70 | # Here we handle a 3D Secure required error by redirecting to an authentication page
71 | if transaction_error['code'] == 'three_d_secure_action_required':
72 | action_token_id = transaction_error['three_d_secure_action_token_id']
73 | return redirect("/3d-secure/authenticate.html#token_id=" + recurly_token_id + "&action_token_id=" + action_token_id + "&account_code=" + str(recurly_account_code))
74 |
75 | return error_redirect(error.error.message)
76 | # Here we may wish to log the API error and send the
77 | # customer to an appropriate URL, perhaps including
78 | # and error message. See the `error_redirect`
79 | # function below.
80 | except recurly.ApiError as error:
81 | return error_redirect(error.error.message)
82 |
83 | # POST route to handle a new account form
84 | @app.route("/api/accounts/new", methods=['POST'])
85 | def new_account():
86 | try:
87 | # Access or generate account_code
88 | if 'recurly-account-code' in request.form:
89 | recurly_account_code = request.form['recurly-account-code']
90 | else:
91 | recurly_account_code = uuid.uuid1()
92 |
93 | account_create = {
94 | "code": recurly_account_code,
95 | "first_name": request.form['first-name'],
96 | "last_name": request.form['last-name'],
97 | "billing_info": { "token_id": request.form['recurly-token']}
98 | }
99 | account = client.create_account(account_create)
100 | return redirect(SUCCESS_URL)
101 | except recurly.ApiError as error:
102 | return error_redirect(error.error.message)
103 |
104 |
105 | # PUT route to handle an account update form
106 | @app.route("/api/accounts/", methods=['PUT'])
107 | def update_account(account_code):
108 | try:
109 | account_update = {
110 | "billing_info": recurly.BillingInfo(
111 | token_id = request.form['recurly-token']
112 | )
113 | }
114 | account = client.update_account("code-%s" % account_code, account_update)
115 | return redirect(SUCCESS_URL)
116 | except recurly.ApiError as error:
117 | return error_redirect(error.error.message)
118 |
119 |
120 | # This endpoint provides configuration to recurly.js
121 | @app.route("/config", methods=['GET'])
122 | def config_js():
123 | return Response("window.recurlyConfig = { publicKey: '" + RECURLY_PUBLIC_KEY + "' }", mimetype='application/javascript')
124 |
125 | # A few utility functions for error handling
126 | def error_redirect(message):
127 | return redirect(ERROR_URL + '?errors=' + message)
128 |
--------------------------------------------------------------------------------
/api/python/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | app:
4 | build:
5 | context: ../..
6 | dockerfile: ./api/python/Dockerfile
7 | volumes:
8 | - .:/usr/src/app/python
9 | - ../../public:/usr/src/app/public
10 | ports:
11 | - "9001:9001"
12 | env_file:
13 | - ../../docker.env
14 | environment:
15 | - PUBLIC_DIR_PATH=/usr/src/app/public
16 |
--------------------------------------------------------------------------------
/api/python/requirements.txt:
--------------------------------------------------------------------------------
1 | Flask
2 | recurly
3 |
--------------------------------------------------------------------------------
/api/ruby/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM ruby:3
2 |
3 | ENV PORT 9001
4 | ENV PUBLIC_DIR_PATH public
5 |
6 | WORKDIR /usr/src/app
7 |
8 | COPY ./api/ruby ./api/ruby
9 | COPY ./public ./public
10 |
11 | RUN cd api/ruby && bundle install
12 |
13 | EXPOSE $PORT
14 |
15 | CMD ruby api/ruby/app.rb -p $PORT
16 |
--------------------------------------------------------------------------------
/api/ruby/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gem 'sinatra', '~> 3.0'
4 | gem 'recurly', '~> 4.0'
5 | gem 'dotenv'
6 | gem 'puma'
7 |
--------------------------------------------------------------------------------
/api/ruby/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | dotenv (2.8.1)
5 | mustermann (3.0.0)
6 | ruby2_keywords (~> 0.0.1)
7 | nio4r (2.5.9)
8 | puma (6.2.2)
9 | nio4r (~> 2.0)
10 | rack (2.2.6.4)
11 | rack-protection (3.0.6)
12 | rack
13 | recurly (4.33.0)
14 | ruby2_keywords (0.0.5)
15 | sinatra (3.0.6)
16 | mustermann (~> 3.0)
17 | rack (~> 2.2, >= 2.2.4)
18 | rack-protection (= 3.0.6)
19 | tilt (~> 2.0)
20 | tilt (2.1.0)
21 |
22 | PLATFORMS
23 | ruby
24 |
25 | DEPENDENCIES
26 | dotenv
27 | puma
28 | recurly (~> 4.0)
29 | sinatra (~> 3.0)
30 |
31 | BUNDLED WITH
32 | 2.4.10
33 |
--------------------------------------------------------------------------------
/api/ruby/README.md:
--------------------------------------------------------------------------------
1 | ## API example: Ruby + Sinatra
2 |
3 | This small application demonstrates how you might set up a web server
4 | using Ruby and [Sinatra][sinatra] with RESTful routes to accept your Recurly.js
5 | form submissions and use the tokens to create and update customer billing
6 | information without having to handle credit card data.
7 |
8 | This example makes use of the official Recurly [Ruby client library][client]
9 | for API v3.
10 |
11 | Note that it is not necessary to use the Sinatra framework. In this example it is
12 | used to organize various API actions into distinct application routes, but one
13 | could just as easily implement these API actions within Rails or any other
14 | application framework.
15 |
16 | ### Routes
17 |
18 | - `POST` [/api/subscriptions/new](app.rb#L37-41)
19 | - `POST` [/api/purchases/new](app.rb#L44-105)
20 | - `POST` [/api/accounts/new](app.rb#L108-120)
21 | - `PUT` [/api/accounts/:account_code](app.rb#L123-136)
22 |
23 | ### Use
24 |
25 | #### Docker
26 |
27 | 1. If you haven't already, [install docker](https://www.docker.com/get-started).
28 |
29 | 2. Update the values in docker.env at the (root of the repo)[https://github.com/recurly/recurly-integration-examples/blob/main/docker.env]
30 |
31 | 3. Run `docker-compose up --build`
32 |
33 | 4. Open [http://localhost:9001](http://localhost:9001)
34 |
35 | #### Local
36 |
37 | 1. Start the server
38 |
39 | ```bash
40 | $ bundle
41 | $ ruby app.rb
42 | ```
43 | 2. Open [http://localhost:9001](http://localhost:9001)
44 |
45 | [sinatra]: http://sinatrarb.com/
46 | [client]: http://github.com/recurly/recurly-client-ruby
47 |
--------------------------------------------------------------------------------
/api/ruby/app.rb:
--------------------------------------------------------------------------------
1 | # Require sinatra and the recurly gem
2 | require 'sinatra'
3 | require 'json'
4 | require 'recurly'
5 | require 'dotenv/load'
6 |
7 | # Used to parse URIs
8 | require 'uri'
9 | # Used to create unique account_codes
10 | require 'securerandom'
11 |
12 | set :bind, '0.0.0.0'
13 | set :port, ENV['PORT'] || 9001
14 | set :public_folder, ENV['PUBLIC_DIR_PATH'] || '../../public'
15 |
16 | enable :logging
17 |
18 | success_url = ENV['SUCCESS_URL']
19 |
20 | client = Recurly::Client.new(api_key: ENV['RECURLY_API_KEY'])
21 |
22 | # Generic error handling
23 | # Here we log the API error and send the
24 | # customer to the error URL, including an error message
25 | def handle_error e
26 | logger.error e
27 | error_uri = URI.parse ENV['ERROR_URL']
28 | error_query = URI.decode_www_form(String(error_uri.query)) << ['error', e.message]
29 | error_uri.query = URI.encode_www_form(error_query)
30 | redirect error_uri.to_s
31 | end
32 |
33 | # POST route to handle a new subscription form
34 | post '/api/subscriptions/new' do
35 | logger.info params
36 | # DEPRECATED: use /api/purchases/new and specify the subscriptions[][plan-code]
37 | redirect '/api/purchases/new?subscriptions[][plan-code]=basic', 307
38 | end
39 |
40 | # POST route to handle a new purchase form
41 | post '/api/purchases/new' do
42 | # This is not a good idea in production but helpful for debugging
43 | # These params may contain sensitive information you don't want logged
44 | logger.info params
45 |
46 | recurly_account_code = params['recurly-account-code'] || SecureRandom.uuid
47 |
48 | recurly_token_id = params['recurly-token']
49 | billing_info = { token_id: recurly_token_id }
50 | # Optionally add a 3D Secure token if one is present. You only need to do this
51 | # if you are integrating with Recurly's 3D Secure support
52 | unless params.fetch('three-d-secure-token', '').empty?
53 | billing_info['three_d_secure_action_result_token_id'] = params['three-d-secure-token']
54 | end
55 |
56 | purchase_create = {
57 | currency: params.fetch('currency', 'USD'),
58 | # This can be an existing account or a new acocunt
59 | account: {
60 | code: recurly_account_code,
61 | first_name: params['first-name'],
62 | last_name: params['last-name'],
63 | billing_info: billing_info
64 | }
65 | }
66 |
67 | subscriptions = params['subscriptions']&.map do |sub_params|
68 | if !sub_params['plan-code'].empty?
69 | { plan_code: sub_params['plan-code'] }
70 | else
71 | nil
72 | end
73 | end.compact
74 | # Add subscriptions to the request if there are any
75 | purchase_create[:subscriptions] = subscriptions if subscriptions&.any?
76 |
77 | line_items = params['items']&.map do |item_params|
78 | {
79 | item_code: item_params['item-code'],
80 | revenue_schedule_type: 'at_invoice'
81 | }
82 | end
83 | # Add line_items to the request if there are any
84 | purchase_create[:line_items] = line_items if line_items&.any?
85 |
86 | begin
87 | purchase = client.create_purchase(body: purchase_create)
88 |
89 | transaction = purchase.charge_invoice&.transactions&.first
90 | if transaction
91 | if transaction.respond_to?(:action_result) # Not yet available in Recurly API
92 | action_result = transaction.action_result
93 | else
94 | action_result = transaction.gateway_response_values['action_result']
95 | action_result = JSON.parse(action_result) if action_result.is_a?(String)
96 | end
97 | end
98 |
99 | if action_result
100 | content_type :json
101 | { action_result: action_result }.to_json
102 | else
103 | redirect(success_url)
104 | end
105 | rescue Recurly::Errors::TransactionError => e
106 | txn_error = e.recurly_error.transaction_error
107 | hash_params = {
108 | token_id: recurly_token_id,
109 | action_token_id: txn_error.three_d_secure_action_token_id,
110 | account_code: recurly_account_code
111 | }.map { |k, v| "#{k}=#{v}" }.join('&')
112 | redirect "/3d-secure/authenticate.html##{hash_params}"
113 | rescue Recurly::Errors::APIError => e
114 | # Here we may wish to log the API error and send the customer to an appropriate URL, perhaps including an error message
115 | handle_error e
116 | end
117 | end
118 |
119 | # POST route to handle a new account form
120 | post '/api/accounts/new' do
121 | begin
122 | client.create_account(body: {
123 | code: SecureRandom.uuid,
124 | billing_info: {
125 | token_id: params['recurly-token']
126 | }
127 | })
128 | redirect success_url
129 | rescue Recurly::Errors::APIError => e
130 | handle_error e
131 | end
132 | end
133 |
134 | # PUT route to handle an account update form
135 | post '/api/accounts/:account_code' do
136 | begin
137 | client.update_account(
138 | account_id: "code-#{params[:account_code]}",
139 | body: {
140 | billing_info: {
141 | token_id: params['recurly-token']
142 | }
143 | }
144 | )
145 | rescue Recurly::Errors::APIError => e
146 | handle_error e
147 | end
148 | end
149 |
150 | post '/tax' do
151 | { rate: 0.05 }.to_json
152 | end
153 |
154 | # This endpoint provides configuration to recurly.js
155 | get '/config' do
156 | items = [].tap do |items|
157 | client.list_items(params: { limit: 200, state: 'active' }).each do |item|
158 | items << { code: item.code, name: item.name }
159 | end
160 | end
161 | plans = [].tap do |plans| client.list_plans(params: { limit: 200, state: 'active' }).each do |plan|
162 | plans << { code: plan.code, name: plan.name }
163 | end
164 | end
165 |
166 | recurly_config = {
167 | publicKey: ENV['RECURLY_PUBLIC_KEY'],
168 | items: items,
169 | plans: plans
170 | }
171 |
172 | adyen_config = {
173 | publicKey: ENV['ADYEN_PUBLIC_KEY'],
174 | }
175 |
176 | content_type :js
177 | "window.recurlyConfig = #{recurly_config.to_json};" \
178 | "window.adyenConfig = #{adyen_config.to_json}"
179 | end
180 |
181 | # All other routes will be treated as static requests
182 | get '*' do
183 | send_file File.join(settings.public_folder, request.path, 'index.html')
184 | end
185 |
--------------------------------------------------------------------------------
/api/ruby/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | app:
4 | build:
5 | context: ../..
6 | dockerfile: ./api/ruby/Dockerfile
7 | volumes:
8 | - .:/usr/src/app/api/ruby
9 | - ../../public:/usr/src/app/public
10 | ports:
11 | - "9001:9001"
12 | env_file:
13 | - ../../docker.env
14 | environment:
15 | - PUBLIC_DIR_PATH=/usr/src/app/public
16 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Recurly Integration Examples",
3 | "description": "For quickly getting started with a Recurly integration.",
4 | "repository": "https://github.com/recurly/recurly-integration-examples",
5 | "keywords": ["recurly", "ruby", "payments", "subscriptions"],
6 | "env": {
7 | "RECURLY_API_KEY": {
8 | "description": "The PRIVATE key from at https://app.recurly.com/go/integrations/api_keys",
9 | "required": true
10 | },
11 | "RECURLY_PUBLIC_KEY": {
12 | "description": "The PUBLIC key from https://app.recurly.com/go/integrations/api_keys",
13 | "required": true
14 | },
15 | "SUCCESS_URL": {
16 | "description": "Where your customers will be redirected if their subscription is successfully created",
17 | "required": true
18 | },
19 | "ERROR_URL": {
20 | "description": "Where your customers will be redirected if there is a problem with their submission",
21 | "required": true
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/docker.env:
--------------------------------------------------------------------------------
1 | # Change these values to configure for running any of the examples with docker
2 | RECURLY_SUBDOMAIN=
3 | RECURLY_API_KEY=
4 | RECURLY_PUBLIC_KEY=
5 | SUCCESS_URL=
6 | ERROR_URL=
7 |
--------------------------------------------------------------------------------
/electron/app.js:
--------------------------------------------------------------------------------
1 | const { app, BrowserWindow } = require('electron');
2 | const express = require('express');
3 | const { createProxyMiddleware } = require('http-proxy-middleware');
4 | const { MY_APP_URL = 'http://localhost:9001', PORT = 3005 } = process.env;
5 |
6 | function createWindow () {
7 | const win = new BrowserWindow({
8 | width: 800,
9 | height: 600
10 | });
11 |
12 | win.loadURL(`http://localhost:${PORT}/index.html`);
13 | }
14 |
15 | function startServer () {
16 | const app = express();
17 |
18 | app.use(express.static('../public'));
19 | app.use('*', createProxyMiddleware({ target: MY_APP_URL, changeOrigin: true }));
20 | app.listen(PORT);
21 | }
22 |
23 | app.whenReady().then(startServer).then(createWindow);
24 |
--------------------------------------------------------------------------------
/electron/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "recurly-electron-example",
3 | "version": "0.0.0",
4 | "description": "An example app using electron with Recurly.js",
5 | "main": "app.js",
6 | "scripts": {
7 | "start": "electron ."
8 | },
9 | "license": "MIT",
10 | "devDependencies": {
11 | "electron": "^15.5.5"
12 | },
13 | "dependencies": {
14 | "express": "^4.17.1",
15 | "http-proxy-middleware": "^1.0.5"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/electron/readme.md:
--------------------------------------------------------------------------------
1 | ## Electron example
2 |
3 | This small application demonstrates how you might set up [Electron](https://www.electronjs.org/) with [Recurly.js](https://developers.recurly.com/reference/recurly-js/index.html).
4 |
5 | The Electron example app needs a separate backend remote server to send requests to Recurly's API. **We do this so we don't expose our private Recurly API key in the Electron distributable.**
6 |
7 | This app starts by instantiating a local express server. The express server has two purposes:
8 |
9 | 1. Serve a static directory of HTML files for the Electron app to render.
10 | 2. Proxy requests to the remote server.
11 |
12 | ### Use
13 |
14 | 1. Start any of the other servers in /api/
15 | 2. Install the Electron app's dependencies
16 |
17 | ```
18 | npm install
19 | ```
20 |
21 | 3. Start the Electron app with `npm start` with the environment variable `MY_APP_URL` set to your remote server's URL. By default, this value will be set to 'http://localhost:9001'
22 |
23 | ```
24 | MY_APP_URL=https://my-app.com npm start
25 | ```
26 |
--------------------------------------------------------------------------------
/netlify.toml:
--------------------------------------------------------------------------------
1 | [build]
2 | publish = "public"
3 | functions = "api/netlify/functions"
4 | command = "cd api/netlify && npm install"
5 |
6 | [template.environment]
7 | RECURLY_API_KEY = "Your Recurly PRIVATE key"
8 | RECURLY_PUBLIC_KEY = "Your Recurly PUBLIC key"
9 | SUCCESS_URL = "Redirect URL for new suscription success"
10 | ERROR_URL = "Redirect URL for new subscription error"
11 |
12 | [[redirects]]
13 | from = "/config"
14 | to = "/.netlify/functions/config"
15 | status = 200
16 |
17 | [[redirects]]
18 | from = "/api/subscriptions/new"
19 | to = "/.netlify/functions/new-subscription"
20 | status = 200
21 |
22 | [[redirects]]
23 | from = "/api/accounts/new"
24 | to = "/.netlify/functions/new-account"
25 | status = 200
26 |
27 | [[redirects]]
28 | from = "/api/accounts/:account"
29 | to = "/.netlify/functions/update-account/:account"
30 | status = 200
31 |
--------------------------------------------------------------------------------
/public/3d-secure/authenticate.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Recurly.js Example: 3-D Secure
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
25 |
26 |
27 |
28 |
29 | $10
30 | monthly
31 |
32 |
33 |
34 |
35 |
36 |
37 | Your bank requires authentication using 3D Secure.
38 |
39 |
40 |
41 |
50 |
51 |
121 |
122 |
123 |
--------------------------------------------------------------------------------
/public/3d-secure/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Recurly.js Example: 3-D Secure
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | $10
19 | monthly
20 |
21 |
22 |
23 |
24 |
25 |
26 |
43 |
44 |
45 |
46 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/public/README.md:
--------------------------------------------------------------------------------
1 | ### Payment form examples
2 |
3 | Here you may find a few examples of payment forms using Recurly.js to tokenize
4 | billing info before submitting to their respective backend.
5 |
6 | ### React examples
7 |
8 | If you plan to use React, check out our [react-recurly][react-recurly-repo] library. We maintain
9 | an example integration of `react-recurly` in the documentation for that library. Be sure to read
10 | through the [documentation][react-recurly-docs] as you explore the [examples][react-recurly-demo].
11 |
12 | [react-recurly-repo]: https://github.com/recurly/react-recurly
13 | [react-recurly-docs]: https://recurly.github.io/react-recurly
14 | [react-recurly-demo]: https://recurly.github.io/react-recurly/?path=/docs/introduction-interactive-demo--page
15 |
--------------------------------------------------------------------------------
/public/account-update/done-white-18dp.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/account-update/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Recurly.js Example: Account Update
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
31 |
32 |
33 |
34 |
35 |
36 | $10
37 | monthly
38 |
39 |
40 |
41 |
42 |
43 |
44 |
Update an account by using the form below.
45 |
46 |
97 |
98 |
*Be sure to include actual user authentication in a real-world implementation!