Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions with-java-springboot/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# https://docs.polar.sh/integrate/oat
POLAR_ACCESS_TOKEN="polar_oat_..."
# https://docs.polar.sh/integrate/webhooks/endpoints#setup-webhooks
POLAR_WEBHOOK_SECRET="polar_whs_..."
# Polar server mode - production or sandbox
POLAR_MODE="sandbox"
# client url - this is the URL the customer would be led to if they purchase something.
POLAR_SUCCESS_URL="http://localhost:8080"
8 changes: 8 additions & 0 deletions with-java-springboot/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/target/
/.env
/.idea/
/*.iml
/.vscode/
/node_modules/
.DS_Store
/.mvn/
3 changes: 3 additions & 0 deletions with-java-springboot/.mvn/wrapper/maven-wrapper.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
wrapperVersion=3.3.4
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip
29 changes: 29 additions & 0 deletions with-java-springboot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
![](../logo.svg)

# Getting started with Polar and Java Spring Boot

## Clone the repository

```bash
npx degit polarsource/examples/with-java-springboot ./with-java-springboot
```

## How to use

1. Run the command below to copy the `.env.example` file:

```bash
cp .env.example .env
```

2. Run the command below to install project dependencies:

```bash
mvn clean install
```

3. Run the Spring Boot application using the following command:

```bash
mvn spring-boot:run
```
67 changes: 67 additions & 0 deletions with-java-springboot/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.1</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>polar-springboot-example</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>polar-springboot-example</name>
<description>Polar with Spring Boot Example</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>io.github.cdimascio</groupId>
<artifactId>dotenv-java</artifactId>
<version>3.1.0</version>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.example.polar;

import io.github.cdimascio.dotenv.Dotenv;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class PolarApplication {

public static void main(String[] args) {
SpringApplication.run(PolarApplication.class, args);
}

@Bean
public Dotenv dotenv() {
return Dotenv.configure()
.ignoreIfMissing()
.load();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package com.example.polar.controller;

import com.example.polar.service.PolarService;
import com.example.polar.util.WebhookVerifier;
import com.fasterxml.jackson.databind.JsonNode;
import io.github.cdimascio.dotenv.Dotenv;
import org.springframework.web.servlet.view.RedirectView;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

@Controller
public class PolarController {

private final PolarService polarService;
private final String webhookSecret;
private final String successUrl;

public PolarController(PolarService polarService, Dotenv dotenv) {
this.polarService = polarService;
this.webhookSecret = dotenv.get("POLAR_WEBHOOK_SECRET");
this.successUrl = dotenv.get("POLAR_SUCCESS_URL");
}

@GetMapping("/")
@ResponseBody
public String home() {
try {
JsonNode products = polarService.listProducts();
StringBuilder html = new StringBuilder();
html.append("<html><body>")
.append("<form action=\"/portal\" method=\"get\">")
.append("<input type=\"email\" name=\"email\" placeholder=\"Email\" required />")
.append("<button type=\"submit\">Open Customer Portal</button>")
.append("</form>");

if (products != null) {
JsonNode items = products.get("items");
if (items != null && items.isArray()) {
for (JsonNode item : items) {
String id = item.get("id").asText();
String name = item.get("name").asText();
html.append("<div><a target=\"_blank\" href=\"/checkout?products=")
.append(id).append("\">").append(name).append("</a></div>");
}
}
}

html.append("</body></html>");
return html.toString();
} catch (Exception e) {
return "<html><body>Error: " + e.getMessage() + "</body></html>";
}
}

@GetMapping("/checkout")
public Object checkout(@RequestParam(name = "products") String products, HttpServletRequest request) {
try {
String productId = products;
String host = request.getHeader("host");
String effectiveSuccessUrl = (successUrl != null && !successUrl.isEmpty())
? successUrl
: "http://" + host + "/";

JsonNode checkout = polarService.createCheckout(productId, effectiveSuccessUrl);
if (checkout == null || !checkout.has("url")) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Failed to create checkout session");
}
return new RedirectView(checkout.get("url").asText());
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error: " + e.getMessage());
}
}

@GetMapping("/portal")
public Object portal(@RequestParam String email) {
try {
JsonNode customers = polarService.findCustomerByEmail(email);
if (customers == null || !customers.has("items")) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Customer not found or API error");
}

JsonNode items = customers.get("items");
if (items == null || !items.isArray() || items.size() == 0) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Customer not found");
}

String customerId = items.get(0).get("id").asText();
JsonNode session = polarService.createCustomerSession(customerId);
if (session == null || !session.has("customer_portal_url")) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Failed to create customer session");
}
return new RedirectView(session.get("customer_portal_url").asText());
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error: " + e.getMessage());
}
}

@PostMapping("/polar/webhooks")
@ResponseBody
public ResponseEntity<String> webhooks(@RequestBody String body, HttpServletRequest request) {
String id = request.getHeader("webhook-id");
String timestamp = request.getHeader("webhook-timestamp");
String signature = request.getHeader("webhook-signature");

boolean isValid = WebhookVerifier.verify(body, id, timestamp, signature, webhookSecret);
if (!isValid) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Invalid signature");
}

System.out.println("Received webhook: " + body);
return ResponseEntity.ok(body);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.example.polar.service;

import io.github.cdimascio.dotenv.Dotenv;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.Map;

@Service
public class PolarService {

private final RestClient restClient;
private final String serverUrl;

public PolarService(Dotenv dotenv) {
String accessToken = dotenv.get("POLAR_ACCESS_TOKEN");
String mode = dotenv.get("POLAR_MODE", "production");

System.out.println("PolarService initialized with Mode: " + mode);
if (accessToken == null || accessToken.isEmpty()) {
System.err.println("CRITICAL: POLAR_ACCESS_TOKEN is missing!");
} else {
System.out.println("POLAR_ACCESS_TOKEN found (starts with: "
+ accessToken.substring(0, Math.min(10, accessToken.length())) + "...)");
}

this.serverUrl = mode.equals("sandbox")
? "https://sandbox-api.polar.sh"
: "https://api.polar.sh";

this.restClient = RestClient.builder()
.baseUrl(this.serverUrl)
.defaultHeader("Authorization", "Bearer " + accessToken)
.defaultHeader("Content-Type", "application/json")
.build();
}

public JsonNode listProducts() {
try {
return restClient.get()
.uri("/v1/products/?is_archived=false")
.retrieve()
.body(JsonNode.class);
} catch (Exception e) {
System.err.println("Error in listProducts: " + e.getMessage());
return null;
}
}

public JsonNode createCheckout(String productId, String successUrl) {
try {
return restClient.post()
.uri("/v1/checkouts/")
.body(Map.of(
"products", new String[] { productId },
"success_url", successUrl))
.retrieve()
.body(JsonNode.class);
} catch (Exception e) {
System.err.println("Error in createCheckout: " + e.getMessage());
return null;
}
}

public JsonNode findCustomerByEmail(String email) {
try {
return restClient.get()
.uri(uriBuilder -> uriBuilder
.path("/v1/customers/")
.queryParam("email", email)
.build())
.retrieve()
.body(JsonNode.class);
} catch (Exception e) {
System.err.println("Error in findCustomerByEmail: " + e.getMessage());
return null;
}
}

public JsonNode createCustomerSession(String customerId) {
try {
return restClient.post()
.uri("/v1/customer-sessions/")
.body(Map.of("customer_id", customerId))
.retrieve()
.body(JsonNode.class);
} catch (Exception e) {
System.err.println("Error in createCustomerSession: " + e.getMessage());
return null;
}
}
}
Loading