Skip to content
Merged
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## 19.5.0
Project ID: the key the SDK is initialized with is now called the Project ID everywhere (SDK docs, README, dashboard). The value is the same, so every existing key keeps working and nothing has to change. `Gleap.initialize(String projectId, Application application)` keeps its signature; only the parameter name and its Javadoc changed. Log lines say "Project ID" instead of "SDK key" / "API key".

The JWT provider restores a signed session after an app restart. `setUserJwt` keeps the token in memory only, so after a restart the SDK holds the session but no token, and the provider was only asked when a token expired. Now, when the session answer says the project runs Signed (`identity.mode: "signed"`, `signed: false`) for an identified session that has no grant (missing, or expired and dropped by the server's answer), no token or user hash is at hand and a provider is set, the SDK asks the provider once per app start and sends its token through the normal `setUserJwt` path. That identify is the one signed identify of the session (never a loop); the provider's rules still apply (one call at a time, 10 second timeout, 30 second pause after a failure); a null token sends nothing; a logout, an identify of the app or a grant arriving while the provider runs drops its token; a token for another user goes through the `identity_mismatch` recovery. `setUserJwtProvider` called after the session answer runs the same check, so the provider works whether it is set before or after `Gleap.initialize`.

## 19.4.0
Customer identity: `Gleap.getInstance().setUserJwt(String)` identifies the user with one HS256 JWT signed by your backend. The SDK reads `userId` (or `user_id`) from the payload without checking the signature and sends the identify with the token as the user hash and no other contact fields; a token that is not three base64url parts, has no user id or is longer than 4096 characters is refused with a log line and nothing is sent. The token stays in memory only (it is never written to the session store) and is sent again whenever it says something new (another claim or key), even when nothing else did (`GleapSessionProperties.equals` compares the token's `kid` and claims without iat/exp/nbf/jti, so a token re-minted per app start for the same user and data is not sent again). `setUserJwtProvider(GleapUserJwtProvider)` fetches a fresh token (`callback.onToken(token)`) when the server answers `token_expired` (the identify is retried once) and before the identify is sent again (file session refresh, retry) when the token expires within 5 minutes; one call at a time, 10 second timeout, 30 second pause after a failure. An identify refused with `token_expired` or `token_invalid` keeps the session and the user (a warning is logged) instead of starting over as a guest; `signature_required` and other refusals behave as before (`signature_required` now logs why). `setAuthTokens(Map<String, String>)` stores per-user tokens for API tools on the signed contact (`POST /sessions/auth-tokens`, a null value removes one), keeps them in memory and sends them again after a signed identify on a new session. `clearIdentity` drops the token and the auth tokens; the provider stays.

Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,18 @@ import io.gleap.Gleap;
The last step is to initialize the Gleap SDK by adding the following Code to the ```onCreate``` method:

```
Gleap.initialize("YOUR_API_KEY", this);
Gleap.initialize("YOUR_PROJECT_ID", this);
```

(Your API key can be found in the project settings within Gleap)
Your Project ID is shown in the Gleap dashboard on the SDK install page and under Settings → Project. It used to be called "SDK key" or "API key": the value is the same, so existing keys keep working.

## Data regions

Gleap projects are hosted in the EU by default. If your project lives in the US region, select the region **before** calling `Gleap.initialize`:

```
Gleap.getInstance().setRegion("us");
Gleap.initialize("YOUR_API_KEY", this);
Gleap.initialize("YOUR_PROJECT_ID", this);
```

`setRegion` accepts `"eu"` (default) or `"us"` (case-insensitive) and sets all regional hosts at once. Unknown values are ignored with a warning.
Expand Down Expand Up @@ -119,13 +119,14 @@ Without it nothing changes: banners overlay the app as before.

### Signed identity (recommended)

Your backend signs an HS256 JWT with an identity key from Settings → Security → Identity keys (put the key id in the `kid` header; the secret never leaves your server). The `userId` claim is required. Optional claims: `email`, `name`, `company` (`{ id, name }`), `plan`, and provider ids such as `stripeCustomerId`. Set `exp` to at most one hour. Everything in the token lands on the contact as signed data the app cannot change. Pass the token to the SDK after login and on every app start of a logged-in user; `setUserJwt` keeps it in memory only. `setUserJwtProvider` fetches a fresh token when it expires, and `setAuthTokens` hands Kai your own API tokens for this user. See [Customer identity](https://docs.gleap.ai/documentation/guides/customer-identity).
Your backend signs an HS256 JWT with an identity key from Settings → Security → Identity keys (put the key id in the `kid` header; the secret never leaves your server). The `userId` claim is required. Optional claims: `email`, `name`, `company` (`{ id, name }`), `plan`, and provider ids such as `stripeCustomerId`. Set `exp` to at most one hour. Everything in the token lands on the contact as signed data the app cannot change. Pass the token to the SDK after login and on every app start of a logged-in user; `setUserJwt` keeps it in memory only. `setUserJwtProvider` fetches a fresh token when it expires, and also signs the session again after an app restart: when the server reports Signed mode without a valid grant and no token was passed to `setUserJwt`, the provider is asked once per app start and its token is sent like one passed to `setUserJwt` (a null token sends nothing). The provider can be set before or after `Gleap.initialize`. `setAuthTokens` hands Kai your own API tokens for this user. See [Customer identity](https://docs.gleap.ai/documentation/guides/customer-identity).

```java
// After login and on every app start of a logged-in user
Gleap.getInstance().setUserJwt(token);

// Called when the token has expired or is about to; pass null when it failed
// Called when the token has expired or is about to, and once per app start to restore a
// signed session when no token was passed; pass null when it failed
Gleap.getInstance().setUserJwtProvider(callback ->
myBackend.fetchGleapToken(token -> callback.onToken(token))
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public void onError(Throwable error, String context) {
}
});

String sdkKey = "ogWhNhuiZcGWrva5nlDS8l7a78OfaLlV";
String projectId = "ogWhNhuiZcGWrva5nlDS8l7a78OfaLlV";
// Debug builds only: every request goes to a local stub server instead of Gleap, e.g.
// adb shell setprop debug.gleap.stub http://10.0.2.2:8787 (then restart the app).
String stub = BuildConfig.DEBUG ? systemProperty("debug.gleap.stub") : "";
Expand All @@ -56,10 +56,10 @@ public void onError(Throwable error, String context) {
Gleap.getInstance().setBannerUrl(stub + "/outbound");
Gleap.getInstance().setModalUrl(stub + "/outbound/modal");
Gleap.getInstance().setRealtimeHost(android.net.Uri.parse(stub).getAuthority());
sdkKey = "stub-sdk-key";
projectId = "stub-sdk-key";
}

Gleap.initialize(sdkKey, this);
Gleap.initialize(projectId, this);

// The widget callbacks: a capture closes and reopens the widget, which the app does not
// hear about (one widget session).
Expand Down
4 changes: 2 additions & 2 deletions gleap/src/main/java/io/gleap/ConfigLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ protected void onPostExecute(JSONObject result) {
protected JSONObject doInBackground(GleapBug... gleapBugs) {
String sdkKey = GleapConfig.getInstance().getSdkKey();
if (sdkKey == null || sdkKey.trim().isEmpty()) {
GleapLog.e("SDK key is missing in ConfigLoader");
GleapLog.e("Project ID is missing in ConfigLoader");
return new JSONObject();
}

Expand Down Expand Up @@ -111,7 +111,7 @@ public void run() {
GleapCallbacks.getInstance().deliverConfigLoaded(result.getJSONObject("flowConfig"));
GleapCallbacks.getInstance().deliverInitialized();
} else {
GleapErrors.report(new Exception("Config could not be loaded. Incorrect API key."), "Gleap config loader");
GleapErrors.report(new Exception("Config could not be loaded. Incorrect Project ID."), "Gleap config loader");
}
}

Expand Down
23 changes: 17 additions & 6 deletions gleap/src/main/java/io/gleap/Gleap.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,13 @@ public static Gleap getInstance() {
/**
* Auto-configures the Gleap SDK from the remote config.
*
* @param sdkKey The SDK key, which can be found on dashboard.Gleap.io
* @param projectId The Project ID of your Gleap project (formerly called "SDK key" or "API
* key"; existing keys keep working unchanged). Find it in the Gleap
* dashboard under Settings → Project or on the SDK install page.
* @param application used to have context and access to take screenshot
*/
public static void initialize(String sdkKey, Application application) {
GleapInitializer.initialize(sdkKey, application);
public static void initialize(String projectId, Application application) {
GleapInitializer.initialize(projectId, application);
}

public void processOpenPushActions() {
Expand Down Expand Up @@ -619,14 +621,23 @@ static void clearPendingUserJwt() {

/**
* Sets the provider that fetches a fresh identity token when the current one expired or is
* about to. One call at a time, 10 s timeout, 30 s pause after a failure.
* about to, and once per app start to sign the session again when the server reports Signed
* mode without a valid grant and no token was passed to {@link #setUserJwt(String)} (the
* token is kept in memory only, so an app restart has none). One call at a time, 10 s
* timeout, 30 s pause after a failure. Can be set before or after
* {@link #initialize(String, Application)}.
*
* @param provider fetches the token and passes it to {@code callback.onToken(token)}
*/
@Override
public void setUserJwtProvider(GleapUserJwtProvider provider) {
try {
GleapUserJwtProviderGate.getInstance().setProvider(provider);
// Set after the session answer arrived: a signed session can still be restored.
GleapSessionController controller = GleapSessionController.getInstance();
if (provider != null && controller != null) {
controller.identifyWithProviderForSignedModeIfNeeded();
}
} catch (Error | Exception exception) {
handleError(exception, "setUserJwtProvider");
}
Expand Down Expand Up @@ -1125,7 +1136,7 @@ public void setLogFlushHandler(GleapLogFlushHandler handler) {
/**
* This is called, when the config is received from the server. The config is loaded once per
* process: a callback set after it was loaded is called once with the loaded config, posted
* to the main thread. Calling {@link Gleap#initialize} again with the same SDK key hands the
* to the main thread. Calling {@link Gleap#initialize} again with the same Project ID hands the
* loaded config to the set callback again, like on iOS.
*
* @param configLoadedCallback callback which is called
Expand All @@ -1138,7 +1149,7 @@ public void setConfigLoadedCallback(ConfigLoadedCallback configLoadedCallback) {
/**
* This is called, when Gleap got initialized (the config was received from the server). A
* callback set after that is called once, posted to the main thread. Calling
* {@link Gleap#initialize} again with the same SDK key calls the set callback again, like on
* {@link Gleap#initialize} again with the same Project ID calls the set callback again, like on
* iOS.
*
* @param initializedCallback callback which is called
Expand Down
6 changes: 6 additions & 0 deletions gleap/src/main/java/io/gleap/GleapBaseSessionService.java
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ private void performSessionRequest(int generation) throws Exception {
JSONObject result = GleapHttp.readLastJsonLine(conn.getInputStream());
if (GleapSessionController.getInstance() != null) {
GleapSessionController.getInstance().processSessionActionResult(result, true, true, generation);
// Signed mode without a grant and no token or hash to send: the JWT provider is asked for one.
try {
GleapSessionController.getInstance().identifyWithProviderForSignedModeIfNeeded();
} catch (Error | Exception error) {
GleapErrors.report(error, "identifyWithProviderForSignedMode");
}
}
} catch (Exception e) {
if (GleapSessionController.getInstance() != null) {
Expand Down
2 changes: 1 addition & 1 deletion gleap/src/main/java/io/gleap/GleapInitializer.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ static Application getApplication() {

static void initialize(String sdkKey, Application app) {
if (sdkKey == null || sdkKey.trim().isEmpty()) {
GleapErrors.report(new IllegalArgumentException("Gleap SDK key is missing or empty."), "initialize");
GleapErrors.report(new IllegalArgumentException("Gleap Project ID is missing or empty."), "initialize");
return;
}

Expand Down
61 changes: 61 additions & 0 deletions gleap/src/main/java/io/gleap/GleapSessionController.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ public class GleapSessionController {
// returning user on a project that switched to Signed gets one identify per app start and
// session, never a loop.
private String signedGrantRequestedFor;
// The same answer when no token or hash is at hand (setUserJwt's token lives in memory only,
// so an app restart has none) but a JWT provider is set: the provider is asked once per app
// start for a token to identify with. Reset on logout.
private boolean signedModeProviderAsked;
// A grant is renewed (the last signed identify sent again) this long before it expires.
static final long GRANT_RENEW_MARGIN_MS = 5 * 60 * 1000;
private static final long GRANT_RENEW_MIN_INTERVAL_MS = 60 * 1000;
Expand Down Expand Up @@ -296,6 +300,7 @@ public void clearUserSession() {
gleapSession = null;
isSessionLoaded = false;
identityMode = null;
signedModeProviderAsked = false;
// The grant goes with the session (store.clear removed the stored copy).
setGrantInMemory(null, null);
}
Expand Down Expand Up @@ -401,6 +406,62 @@ boolean claimSignedGrantRequest(GleapSessionProperties identify) {
}
}

/**
* Signed mode on an identified session without a grant, and no token or hash at hand to send
* for one (an app restart after setUserJwt: the token lives in memory only). Holds the lock.
*/
private boolean signedModeWithoutHeldToken() {
synchronized (this) {
GleapSession session = gleapSession;
GleapSessionProperties user = gleapSessionProperties;
if (session == null || user == null || !signedGrantMissing(session.getId(), user.getUserId())) {
return false;
}
// A held token or hash goes out as the held identify instead (see processSessionActionResult).
boolean heldIdentify = lastIdentify != null && lastIdentify.getHash() != null && !lastIdentify.getHash().isEmpty();
boolean heldHash = user.getHash() != null && !user.getHash().isEmpty();
return !heldIdentify && !heldHash;
}
}

/**
* Restores a signed session from the JWT provider alone: when the session answer says the
* project runs Signed, the identified session has no grant and no token or hash is at hand,
* the provider is asked once per app start and its token is sent through
* {@link Gleap#setUserJwt(String)}, like a token from the app (that identify is the one
* signed identify of this session, see {@link #claimSignedGrantRequest}). The provider's
* rules apply (one call at a time, 10 s timeout, 30 s pause after a failure); a null token
* sends nothing. A logout, an identify of the app or a grant arriving meanwhile drops the
* token. Also run when the provider is set after the session answer.
*/
void identifyWithProviderForSignedModeIfNeeded() {
final int generation;
synchronized (this) {
if (signedModeProviderAsked || !isSessionLoaded || pendingIdentificationAction != null
|| GleapIdentifyService.isRunning() || !GleapUserJwtProviderGate.getInstance().hasProvider()
|| !signedModeWithoutHeldToken()) {
return;
}
signedModeProviderAsked = true;
generation = currentGeneration();
}
GleapUserJwtProviderGate.getInstance().requestToken(new GleapUserJwtProviderGate.TokenCallback() {
@Override
public void onResult(String token) {
if (token == null) {
return;
}
synchronized (GleapSessionController.this) {
if (!isCurrentGeneration(generation) || pendingIdentificationAction != null
|| GleapIdentifyService.isRunning() || !signedModeWithoutHeldToken()) {
return;
}
}
Gleap.getInstance().setUserJwt(token);
}
});
}

private void setGrantInMemory(String value, String sessionId) {
if (value == null || value.isEmpty() || sessionId == null || sessionId.isEmpty()) {
grant = null;
Expand Down
Loading
Loading