├── .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 | [![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy) 78 | 79 | [![Run on Google Cloud](https://deploy.cloud.run/button.svg)](https://deploy.cloud.run) 80 | 81 | [![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](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 | 32 |
33 | 34 |
35 | 36 |
37 | Your bank requires authentication using 3D Secure. 38 |
39 | 40 |
41 |
42 |
43 |
44 | Authenticating your payment method... 45 |
46 | 47 | 48 | 49 |
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 | 21 |
22 | 23 |
24 | 25 |
26 |
27 |
28 |
29 |
30 |
31 | 32 | 33 |
34 |
35 | 36 | 37 |
38 | 39 | 40 | 41 | 42 |
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 | 39 |
40 | 41 |
42 | 43 |
44 |

Update an account by using the form below.

45 | 46 |
47 |
48 |
49 |
50 | 51 |
52 | 53 | 54 |
55 | 56 |
57 | 58 | 59 |
60 | 61 |
62 | 63 | 64 |
65 | 66 |
67 | 68 | 69 |
70 | 71 |
72 | 73 | 74 |
75 | 76 |
77 | 78 | 79 |
80 | 81 |
82 | 83 | 84 |
85 | 86 |
87 | 88 | 89 |
90 | 91 | 94 | 95 | 96 |
97 | 98 |

*Be sure to include actual user authentication in a real-world implementation!

99 |
100 |
101 | 102 | 179 | 180 | 181 | -------------------------------------------------------------------------------- /public/advanced-pricing-items/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Recurly.js Example: Advanced Pricing Items 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

18 | 19 | Subscribe 20 | 21 |

22 |
23 | 24 |
25 | 26 |
27 |
28 |
29 | 30 | 31 |
32 | 33 |
34 |
Select items
35 |
36 |
37 | 38 |
39 | 40 | 41 |
42 | 43 |
44 | 45 | 46 |
47 | 48 |
49 |
50 |
51 | 52 |
53 |
Total
54 |
55 |
56 | Due now 57 | 58 | 59 | 60 | 61 |
62 |
63 | 64 | 65 | due next month. 66 |
67 |
68 |
69 | 70 | 71 | 72 | 73 |
74 |
75 |
76 | 77 | 185 | 186 | 187 | -------------------------------------------------------------------------------- /public/advanced-tax/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Recurly.js Example: Advanced VAT 2015 taxation 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 | 18 | 22 | 23 | 24 | 25 | 26 |
27 |
28 |
29 | 30 |
31 | 32 | 33 |
34 | 35 |
36 | 37 | 38 |
39 | 40 | 41 | 47 | 48 | 49 | 50 |
51 |

VAT Number

52 | 53 |
54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |

66 | Due now 67 |

68 |
    69 |
  • 70 | Discount: 71 | 72 | 73 |
  • 74 |
  • 75 | Subtotal: 76 | 77 | 78 |
  • 79 |
  • 80 | Tax: 81 | 82 | 83 |
  • 84 |
  • ----
  • 85 |
  • 86 | Total: 87 | 88 | 89 |
  • 90 |
91 |

92 | Due later 93 |

94 |
    95 |
  • 96 | Discount: 97 | 98 | 99 |
  • 100 |
  • 101 | Subtotal: 102 | 103 | 104 |
  • 105 |
  • 106 | Tax: 107 | 108 | 109 |
  • 110 |
  • ----
  • 111 |
  • 112 | Total: 113 | 114 | 115 |
  • 116 |
117 | 118 |
119 |
120 |
121 | 122 | 214 | 215 | 216 | -------------------------------------------------------------------------------- /public/adyen/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Recurly.js Example: Minimal Billing Information 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /public/alternative-payment-methods/boleto.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Recurly.js Example: Minimal Billing Information 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |

19 | $10 20 | monthly 21 |

22 |
23 | 24 |
25 | 26 |
27 |
28 | 29 | 30 |
31 | 32 | 33 |
34 | 35 |
36 | 37 | 38 |
39 | 40 |
41 |
42 | Loading payment methods... 43 |
44 |
45 | 46 | 47 | 48 | 49 |
50 |
51 |
52 | 53 | 173 | 174 | 175 | 176 | -------------------------------------------------------------------------------- /public/alternative-payment-methods/completed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Recurly.js Example: Alternative Payment Methods - iDEAL 7 | 8 | 9 | 10 | 11 | 12 |
13 |

Thank you

14 | 15 |

Transaction status in Recurly was completed with status "".

16 | 17 | Back to home 18 |
19 | 20 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /public/alternative-payment-methods/ideal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Recurly.js Example: Minimal Billing Information 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |

19 | $10 20 | monthly 21 |

22 |
23 | 24 |
25 | 26 |
27 |
28 | 29 | 30 |
31 | 32 | 33 |
34 | 35 |
36 | 37 | 38 |
39 | 40 |
41 |
42 | Loading payment methods... 43 |
44 |
45 | 46 | 47 | 48 | 49 |
50 |
51 |
52 | 53 | 173 | 174 | 175 | 176 | -------------------------------------------------------------------------------- /public/apple-pay/advanced/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Recurly.js Example: Advanced Apple Pay 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 68 | 69 | 70 |
71 |
72 |

73 | $10 74 | monthly 75 |

76 | 80 |
81 | 82 |
83 | 84 |
85 |
86 |
87 | 88 |
89 |
90 |
91 | 92 | 172 | 173 | 174 | -------------------------------------------------------------------------------- /public/apple-pay/direct-tax/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Recurly.js Example: Apple Pay Direct Tax 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 68 | 69 | 70 |
71 |
72 |

73 | $10 74 | monthly 75 |

76 |
77 | 78 |
79 | 80 |
81 |
82 | 83 | 84 |
85 |
86 | 87 | 88 |
89 |
90 | 91 | 92 |
93 |
94 | 95 | 96 |
97 |
98 | 99 | 100 |
101 |
102 | 103 | 104 |
105 |
106 | 107 | 108 |
109 |
110 | 111 |
112 |
113 | 114 | 201 | 202 | 203 | -------------------------------------------------------------------------------- /public/apple-pay/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Recurly.js Example: Minimal Billing Information 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 68 | 69 | 70 |
71 |
72 |

73 | $10 74 | monthly 75 |

76 |
77 | 78 |
79 | 80 |
81 |
82 |
83 | 84 |
85 |
86 |
87 | 88 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /public/apple-pay/shipping/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Recurly.js Example: Apple Pay Shipping Validation 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 68 | 69 | 70 |
71 |
72 |

73 | $10 74 | monthly 75 |

76 |
77 | 78 |
79 | 80 |
81 |
82 | 83 | 84 |
85 |
86 | 87 |
88 |
89 | 90 | 165 | 166 | 167 | -------------------------------------------------------------------------------- /public/auto-tab/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Recurly.js Example: Auto-tabbing with Elements 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |

17 | $10 18 | monthly 19 |

20 |
21 | 22 |
23 | 24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | 33 |
34 | 35 | 36 |
37 | 38 |
39 | 40 | 41 |
42 | 43 | 44 | 45 | 46 |
47 |
48 |
49 | 50 | 175 | 176 | 177 | -------------------------------------------------------------------------------- /public/bank-redirect/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Bank Redirect Example 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Bank Redirect

14 |
15 |
16 | Payment Method Type: 17 | 20 |
21 | 22 |
23 | Invoice UUID: 24 | 25 |
26 | 27 |
28 | Bank: 29 | 30 |
31 | 32 | 33 |
34 | 35 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /public/coBadged/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Recurly.js Example: Co-Badged Support 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |

16 | $10 17 | monthly 18 |

19 |
20 | 21 |
22 | 23 |
24 |
25 | 26 |
27 | 28 | 29 |
30 | 31 |
32 | 33 | 34 |
35 | 36 |
37 |
38 |
39 | 40 | 41 | 42 | 43 |
44 |
45 |
46 | 47 | 170 | 171 | 172 | -------------------------------------------------------------------------------- /public/fraud-detection/README.md: -------------------------------------------------------------------------------- 1 | ### Fraud detection examples 2 | 3 | Recurly.js supports enhanced fraud detction based on customer device data. There are two examples 4 | here. 5 | 6 | If you are using the [Recurly fraud management](https://docs.recurly.com/v1.0/docs/fraud-management) 7 | or [Kount direct fraud management](https://docs.recurly.com/v1.0/docs/kount) integrations, 8 | see [this example](recurly-kount.html) 9 | 10 | If you are using the Braintree fraud management integration, first be sure to familiarize yourself with 11 | [their client-side data collector documentation](https://developers.braintreepayments.com/guides/advanced-fraud-tools/client-side/javascript/v3), 12 | then see [this example](braintree.html) 13 | -------------------------------------------------------------------------------- /public/fraud-detection/braintree.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Recurly.js Example: Minimal Billing Information 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |

22 | $10 23 | monthly 24 |

25 |
26 | 27 |
28 | 29 |
30 |
31 |
32 |
33 |
34 | 35 |
36 | 37 | 38 |
39 | 40 |
41 | 42 | 43 |
44 | 45 | 46 | 47 | 48 |
49 |
50 |
51 | 52 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /public/fraud-detection/recurly-kount.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Recurly.js Example: Minimal Billing Information 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 | 21 |
22 | 23 |
24 | 25 |
26 |
27 |
28 |
29 |
30 | 31 |
32 | 33 | 34 |
35 | 36 |
37 | 38 | 39 |
40 | 41 | 42 | 43 | 44 |
45 |
46 |
47 | 48 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /public/gift-cards/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Recurly.js Example: Gift Cards 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 19 |
20 | 21 |
22 | 23 |
24 |
25 |
26 |
27 |
28 | 29 |
30 | 31 | 32 |
33 | 34 |
35 | 36 | 37 |
38 | 39 |
40 | 41 | 42 |
43 | 44 | 45 | 46 | 47 |
48 |
49 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /public/google-pay/direct-tax/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Recurly.js Google Pay 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 18 | 19 | 20 |
21 |
22 |

23 | $10 24 | Purchase 25 |

26 |
27 | 28 |
29 | 30 |
31 |
32 | 33 |
34 |
35 |
36 |
37 | 38 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /public/google-pay/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Recurly.js Google Pay 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 18 | 19 | 20 |
21 |
22 |

23 | $10 24 | Purchase 25 |

26 |
27 | 28 |
29 | 30 |
31 |
32 | 33 |
34 |
35 |
36 |
37 | 38 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Recurly.js example payment forms 5 | 6 | 19 | 20 | 21 |
22 |

Examples

23 | 47 |
48 | 49 | 50 | -------------------------------------------------------------------------------- /public/minimal/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Recurly.js Example: Minimal Billing Information 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |

17 | $10 18 | monthly 19 |

20 |
21 | 22 |
23 | 24 |
25 |
26 |
27 |
28 |
29 | 30 |
31 | 32 | 33 |
34 | 35 |
36 | 37 | 38 |
39 | 40 | 41 | 42 | 43 |
44 |
45 |
46 | 47 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /public/paypal/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Recurly.js Example: Minimal Billing Information 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |

$29/month

16 |
17 |
18 |
19 | 20 | 21 |
22 |
23 |
24 | 25 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html { 6 | min-height: 100%; 7 | } 8 | 9 | body { 10 | background: linear-gradient(-30deg, #E1E3E6, #EBEFF8); 11 | font-family: 'Open Sans', system-ui, sans-serif; 12 | margin: 0; 13 | } 14 | 15 | main { 16 | width: 22rem; 17 | margin: 1rem auto; 18 | } 19 | 20 | section { 21 | text-align: center; 22 | } 23 | 24 | section.errors { 25 | color: #E43C29; 26 | } 27 | 28 | h1 { 29 | font-weight: normal; 30 | } 31 | 32 | label, .label { 33 | color: #2C0730; 34 | font-size: small; 35 | font-weight: bold; 36 | text-align: left; 37 | text-transform: uppercase; 38 | margin: 0 0 0.25rem; 39 | display: block; 40 | } 41 | 42 | input[type='radio'] + label { 43 | display: inline-block; 44 | } 45 | 46 | input[type="text"], input[type="email"], select { 47 | transition: border-color 0.3s; 48 | -webkit-appearance: none; 49 | appearance: none; 50 | outline: none; 51 | box-shadow: none; 52 | /* border-radius: 0; */ 53 | border: 2px solid #C2C2C2; 54 | background: transparent; 55 | color: #c2c2c2; 56 | font-family: inherit; 57 | font-size: inherit; 58 | font-weight: bold; 59 | width: 100%; 60 | padding: 0.5rem; 61 | margin: 0 0 1rem; 62 | display: block; 63 | } 64 | 65 | input:focus { 66 | border-color: currentColor; 67 | color: #2C0730; 68 | } 69 | 70 | input.error { 71 | border-color: #E43C29; 72 | } 73 | 74 | button { 75 | transition-timing-function: cubic-bezier(0.7, 0, 0.3, 1); 76 | transition: opacity 0.3s; 77 | cursor: pointer; 78 | outline: none; 79 | border: none; 80 | background: #2C0730; 81 | color: #F5F5F5; 82 | padding: 1rem; 83 | font-family: inherit; 84 | font-size: inherit; 85 | font-weight: bold; 86 | letter-spacing: 1px; 87 | width: 100%; 88 | } 89 | 90 | button:disabled { 91 | opacity: 0.6; 92 | } 93 | 94 | a { 95 | color: #2C0730; 96 | } 97 | 98 | ul { 99 | list-style: none; 100 | padding-left: 0; 101 | } 102 | 103 | li { 104 | margin: 0.5rem 0; 105 | } 106 | 107 | .logo { 108 | transition: all 0.5s; 109 | background: #F5F5F5; 110 | color: #2C0730; 111 | border-radius: 9rem; 112 | font-size: x-large; 113 | letter-spacing: 1px; 114 | text-align: center; 115 | width: 9rem; 116 | height: 9rem; 117 | line-height: 9rem; 118 | margin: 2rem auto; 119 | } 120 | 121 | .logo .term { 122 | line-height: 1em; 123 | font-size: small; 124 | margin-left: -0.25rem; 125 | } 126 | 127 | @media screen and (max-height: 599px) { 128 | .logo { 129 | border-radius: 0; 130 | width: 100%; 131 | height: 3rem; 132 | line-height: 3rem; 133 | margin: 2rem auto 1rem; 134 | } 135 | } 136 | 137 | .recurly-element { 138 | appearance: none; 139 | -webkit-appearance: none; 140 | transition: border-color 0.3s; 141 | outline: none; 142 | border-radius: 0; 143 | border: 2px solid #c2c2c2; 144 | box-shadow: none; 145 | background: transparent; 146 | color: #c2c2c2; 147 | font-family: inherit; 148 | font-size: 1rem; 149 | font-weight: bold; 150 | width: 100%; 151 | height: 42px; 152 | margin: 0 0 1rem; 153 | position: relative; 154 | } 155 | 156 | .recurly-element-focus { 157 | border-color: #2c0730; 158 | color: #2c0730; 159 | z-index: 10; 160 | } 161 | 162 | .recurly-element-invalid { 163 | border: 2px solid #E43C29; 164 | } 165 | 166 | .items { 167 | margin-bottom: 1.25rem; 168 | } 169 | 170 | .item { 171 | text-align: left; 172 | } 173 | 174 | .item > label { 175 | display: inline; 176 | text-transform: none; 177 | } 178 | 179 | .float-right { 180 | float: right; 181 | } 182 | 183 | .subtotal { 184 | text-align: left; 185 | } 186 | 187 | .subtotal > div { 188 | margin: 0.5rem 0; 189 | } 190 | 191 | .text-muted { 192 | color: gray; 193 | font-size: x-small; 194 | margin: 0.25rem 0; 195 | } 196 | -------------------------------------------------------------------------------- /public/vue/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Recurly.js Example: Minimal Billing Information with Vue 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 | 57 |
58 |
59 | 60 | 98 | 99 | 100 | --------------------------------------------------------------------------------