Skip to content

Commit 911d502

Browse files
feat: add age verification endpoints (0.4.1)
1 parent 93684c2 commit 911d502

5 files changed

Lines changed: 118 additions & 4 deletions

File tree

README.MD

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ dependencyResolutionManagement {
1212

1313
// build.gradle.kts
1414
dependencies {
15-
implementation("com.github.PortalTechnologiesInc:java-sdk:0.4.0")
15+
implementation("com.github.PortalTechnologiesInc:java-sdk:0.4.1")
1616
}
1717
```
1818

@@ -87,6 +87,22 @@ client.deliverWebhookPayload(rawBody, request.getHeader("X-Portal-Signature"));
8787
| `requestCashu(recipientKey, subkeys, mintUrl, unit, amount)` | `AsyncOperation<CashuResponseStatus>` |
8888
| `authenticateKey(mainKey, subkeys)` | `AsyncOperation<AuthResponseData>` |
8989
| `newKeyHandshakeUrl(staticToken, noRequest)` | `AsyncOperation<KeyHandshakeResult>` |
90+
| `createVerificationSession(relayUrls)` | `AsyncOperation<CashuResponseStatus>` |
91+
| `requestVerificationToken(recipientKey, subkeys)` | `AsyncOperation<CashuResponseStatus>` |
92+
93+
### Age Verification
94+
95+
```java
96+
// Create verification session — single call handles everything
97+
AsyncOperation<CashuResponseStatus> op = client.createVerificationSession();
98+
System.out.println("Redirect user to: " + op.sessionUrl());
99+
100+
// Wait for the user to complete verification in their browser
101+
CashuResponseStatus result = client.pollUntilComplete(op, new PollOptions(1000, 300_000));
102+
if (result.status.equals("success")) {
103+
System.out.println("Verified! Token: " + result.token);
104+
}
105+
```
90106

91107
## Sync methods
92108

build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ plugins {
44
}
55

66
group = "cc.getportal"
7-
version = "0.4.0"
7+
version = "0.4.1"
88

99
java {
1010
toolchain {
@@ -23,7 +23,7 @@ publishing {
2323

2424
groupId = "cc.getportal"
2525
artifactId = "portal-java-sdk"
26-
version = "0.4.0"
26+
version = "0.4.1"
2727
}
2828
}
2929
}
Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,43 @@
11
package cc.getportal;
22

3+
import org.jetbrains.annotations.Nullable;
4+
5+
import java.util.Map;
36
import java.util.concurrent.CompletableFuture;
7+
import java.util.concurrent.ConcurrentHashMap;
48

59
/**
610
* Wraps an async operation: the stream ID is available immediately,
711
* while the {@code done} future resolves when a terminal event arrives.
812
*/
9-
public record AsyncOperation<T>(String streamId, CompletableFuture<T> done) {}
13+
public class AsyncOperation<T> {
14+
private final String streamId;
15+
private final CompletableFuture<T> done;
16+
private final Map<String, String> metadata = new ConcurrentHashMap<>();
17+
18+
public AsyncOperation(String streamId, CompletableFuture<T> done) {
19+
this.streamId = streamId;
20+
this.done = done;
21+
}
22+
23+
public String streamId() { return streamId; }
24+
public CompletableFuture<T> done() { return done; }
25+
26+
public void setMetadata(String key, String value) {
27+
metadata.put(key, value);
28+
}
29+
30+
public @Nullable String getMetadata(String key) {
31+
return metadata.get(key);
32+
}
33+
34+
/** Convenience: get the session_url for verification sessions. */
35+
public @Nullable String sessionUrl() {
36+
return metadata.get("session_url");
37+
}
38+
39+
/** Convenience: get the session_id for verification sessions. */
40+
public @Nullable String sessionId() {
41+
return metadata.get("session_id");
42+
}
43+
}

src/main/java/cc/getportal/PortalClient.java

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,58 @@ public WalletInfoResponse getWalletInfo() throws IOException, InterruptedExcepti
486486
return get("/wallet/info", WalletInfoResponse.class);
487487
}
488488

489+
// -------------------------------------------------------------------------
490+
// Verification
491+
// -------------------------------------------------------------------------
492+
493+
/**
494+
* Create an age verification session and automatically start listening for the
495+
* verification token. Returns session info plus an {@link AsyncOperation} that
496+
* resolves when the user completes verification.
497+
*
498+
* <p>Redirect the user to {@code response.session_url} in their browser.
499+
* Poll or await {@code done()} for the {@link CashuResponseStatus} result.
500+
*
501+
* @param relayUrls optional relay URLs; defaults to server's [nostr] config if null
502+
*/
503+
public AsyncOperation<CashuResponseStatus> createVerificationSession(
504+
@Nullable List<String> relayUrls
505+
) throws IOException, InterruptedException, PortalSDKException {
506+
Map<String, Object> body = new java.util.HashMap<>();
507+
if (relayUrls != null) body.put("relays", relayUrls);
508+
VerificationSessionResponse resp = post("/verification/sessions", body, VerificationSessionResponse.class);
509+
AsyncOperation<CashuResponseStatus> op = registerStream(resp.stream_id,
510+
json -> gson.fromJson(json.getAsJsonObject("status"), CashuResponseStatus.class));
511+
op.setMetadata("session_id", resp.session_id);
512+
op.setMetadata("session_url", resp.session_url);
513+
op.setMetadata("ephemeral_npub", resp.ephemeral_npub);
514+
op.setMetadata("expires_at", String.valueOf(resp.expires_at));
515+
return op;
516+
}
517+
518+
/** Convenience overload with default relays. */
519+
public AsyncOperation<CashuResponseStatus> createVerificationSession()
520+
throws IOException, InterruptedException, PortalSDKException {
521+
return createVerificationSession(null);
522+
}
523+
524+
/**
525+
* Request a verification token from a user who already holds one
526+
* (e.g. verified through the mobile app).
527+
*
528+
* @param recipientKey hex-encoded public key of the token holder
529+
* @param subkeys optional subkeys
530+
*/
531+
public AsyncOperation<CashuResponseStatus> requestVerificationToken(
532+
String recipientKey, List<String> subkeys
533+
) throws IOException, InterruptedException, PortalSDKException {
534+
Map<String, Object> body = Map.of("recipient_key", recipientKey, "subkeys", subkeys);
535+
JsonObject resp = post("/verification/token", body, JsonObject.class);
536+
String streamId = resp.get("stream_id").getAsString();
537+
return registerStream(streamId,
538+
json -> gson.fromJson(json.getAsJsonObject("status"), CashuResponseStatus.class));
539+
}
540+
489541
// -------------------------------------------------------------------------
490542
// Events (low-level)
491543
// -------------------------------------------------------------------------
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package cc.getportal.model;
2+
3+
/**
4+
* Response from {@code POST /verification/sessions}.
5+
*/
6+
public class VerificationSessionResponse {
7+
public String session_id;
8+
public String session_url;
9+
public String ephemeral_npub;
10+
public long expires_at;
11+
public String stream_id;
12+
}

0 commit comments

Comments
 (0)