diff --git a/README.MD b/README.MD index c21528c..0aa6709 100644 --- a/README.MD +++ b/README.MD @@ -12,7 +12,7 @@ dependencyResolutionManagement { // build.gradle.kts dependencies { - implementation("com.github.PortalTechnologiesInc:java-sdk:0.4.0") + implementation("com.github.PortalTechnologiesInc:java-sdk:0.4.1") } ``` @@ -87,6 +87,20 @@ client.deliverWebhookPayload(rawBody, request.getHeader("X-Portal-Signature")); | `requestCashu(recipientKey, subkeys, mintUrl, unit, amount)` | `AsyncOperation` | | `authenticateKey(mainKey, subkeys)` | `AsyncOperation` | | `newKeyHandshakeUrl(staticToken, noRequest)` | `AsyncOperation` | +| `createVerificationSession(relayUrls)` | `VerificationSession` | +| `requestVerificationToken(recipientKey, subkeys)` | `AsyncOperation` | + +### Age Verification + +```java +VerificationSession session = client.createVerificationSession(); +System.out.println("Redirect user to: " + session.session.session_url); + +CashuResponseStatus result = client.pollUntilComplete(session.operation, new PollOptions(1000, 300_000)); +if (result.status.equals("success")) { + System.out.println("Verified! Token: " + result.token); +} +``` ## Sync methods diff --git a/build.gradle.kts b/build.gradle.kts index b98dd95..3d92995 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,7 @@ plugins { } group = "cc.getportal" -version = "0.4.0" +version = "0.4.1" java { toolchain { @@ -23,7 +23,7 @@ publishing { groupId = "cc.getportal" artifactId = "portal-java-sdk" - version = "0.4.0" + version = "0.4.1" } } } diff --git a/src/main/java/cc/getportal/PortalClient.java b/src/main/java/cc/getportal/PortalClient.java index 414ae6c..e47d138 100644 --- a/src/main/java/cc/getportal/PortalClient.java +++ b/src/main/java/cc/getportal/PortalClient.java @@ -486,6 +486,55 @@ public WalletInfoResponse getWalletInfo() throws IOException, InterruptedExcepti return get("/wallet/info", WalletInfoResponse.class); } + // ------------------------------------------------------------------------- + // Verification + // ------------------------------------------------------------------------- + + /** + * Create an age verification session and automatically start listening for the + * verification token. Returns a {@link VerificationSession} containing session + * info and an {@link AsyncOperation} that resolves when the user completes + * verification. + * + *

Redirect the user to {@code session.session_url} in their browser. + * Poll or await {@code operation.done()} for the {@link CashuResponseStatus} result. + * + * @param relayUrls optional relay URLs; defaults to server's [nostr] config if null + */ + public VerificationSession createVerificationSession( + @Nullable List relayUrls + ) throws IOException, InterruptedException, PortalSDKException { + Map body = new java.util.HashMap<>(); + if (relayUrls != null) body.put("relays", relayUrls); + VerificationSessionResponse resp = post("/verification/sessions", body, VerificationSessionResponse.class); + AsyncOperation op = registerStream(resp.stream_id, + json -> gson.fromJson(json.getAsJsonObject("status"), CashuResponseStatus.class)); + return new VerificationSession(resp, op); + } + + /** Convenience overload with default relays. */ + public VerificationSession createVerificationSession() + throws IOException, InterruptedException, PortalSDKException { + return createVerificationSession(null); + } + + /** + * Request a verification token from a user who already holds one + * (e.g. verified through the mobile app). + * + * @param recipientKey hex-encoded public key of the token holder + * @param subkeys optional subkeys + */ + public AsyncOperation requestVerificationToken( + String recipientKey, List subkeys + ) throws IOException, InterruptedException, PortalSDKException { + Map body = Map.of("recipient_key", recipientKey, "subkeys", subkeys); + JsonObject resp = post("/verification/token", body, JsonObject.class); + String streamId = resp.get("stream_id").getAsString(); + return registerStream(streamId, + json -> gson.fromJson(json.getAsJsonObject("status"), CashuResponseStatus.class)); + } + // ------------------------------------------------------------------------- // Events (low-level) // ------------------------------------------------------------------------- diff --git a/src/main/java/cc/getportal/model/VerificationSession.java b/src/main/java/cc/getportal/model/VerificationSession.java new file mode 100644 index 0000000..be1a808 --- /dev/null +++ b/src/main/java/cc/getportal/model/VerificationSession.java @@ -0,0 +1,20 @@ +package cc.getportal.model; + +import cc.getportal.AsyncOperation; + +/** + * Result of {@code createVerificationSession()} — contains the session info + * and an {@link AsyncOperation} for polling the verification token. + */ +public class VerificationSession { + public final VerificationSessionResponse session; + public final AsyncOperation operation; + + public VerificationSession( + VerificationSessionResponse session, + AsyncOperation operation + ) { + this.session = session; + this.operation = operation; + } +} diff --git a/src/main/java/cc/getportal/model/VerificationSessionResponse.java b/src/main/java/cc/getportal/model/VerificationSessionResponse.java new file mode 100644 index 0000000..e4dc080 --- /dev/null +++ b/src/main/java/cc/getportal/model/VerificationSessionResponse.java @@ -0,0 +1,12 @@ +package cc.getportal.model; + +/** + * Response from {@code POST /verification/sessions}. + */ +public class VerificationSessionResponse { + public String session_id; + public String session_url; + public String ephemeral_npub; + public long expires_at; + public String stream_id; +}