Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,10 @@ open class MainApplication : Application(), ReactApplication, INotificationsAppl
SoLoader.init(this, OpenSourceMergedSoMapping)
Bugsnag.start(this)

if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
load()
}
// Load entry point for the new architecture
load()

reactNativeHost.reactInstanceManager.addReactInstanceEventListener(object : ReactInstanceEventListener {
reactHost.addReactInstanceEventListener(object : ReactInstanceEventListener {
override fun onReactContextInitialized(context: ReactContext) {
CustomPushNotification.setReactContext(context as ReactApplicationContext)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,15 @@ public void onReceived() throws InvalidNotificationException {
Ejson receivedEjson = safeFromJson(received.getString("ejson", "{}"), Ejson.class);

if (receivedEjson != null && receivedEjson.notificationType != null && receivedEjson.notificationType.equals("message-id-only")) {
android.util.Log.d("RocketChat.CustomPush", "Detected message-id-only notification, will fetch full content from server");
notificationLoad(receivedEjson, new Callback() {
@Override
public void call(@Nullable Bundle bundle) {
if (bundle != null) {
android.util.Log.d("RocketChat.CustomPush", "Successfully loaded notification content from server, updating notification props");
mNotificationProps = createProps(bundle);
} else {
android.util.Log.w("RocketChat.CustomPush", "Failed to load notification content from server, will display placeholder notification");
}
}
});
Expand Down Expand Up @@ -154,6 +158,7 @@ protected Notification.Builder getNotificationBuilder(PendingIntent intent) {

// message couldn't be loaded from server (Fallback notification)
} else {
android.util.Log.w("RocketChat.CustomPush", "Displaying fallback notification for message-id-only (content failed to load from server)");
Gson gson = new Gson();
// iterate over the current notification ids to dismiss fallback notifications from same server
for (Map.Entry<String, List<Bundle>> bundleList : notificationMessages.entrySet()) {
Expand All @@ -168,6 +173,7 @@ protected Notification.Builder getNotificationBuilder(PendingIntent intent) {
String id = not.getString("notId");
// cancel this notification
notificationManager.cancel(Integer.parseInt(id));
android.util.Log.d("RocketChat.CustomPush", "Cancelled previous fallback notification from same server");
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

import java.math.BigInteger;

import chat.rocket.reactnative.BuildConfig;

class RNCallback implements Callback {
public void invoke(Object... args) {

Expand All @@ -29,12 +31,15 @@ static public String toHex(String arg) {
}

public class Ejson {
private static final String TAG = "RocketChat.Ejson";

String host;
String rid;
String type;
Sender sender;
String messageId;
String notificationType;
String messageType;
String senderName;
String msg;

Expand All @@ -45,35 +50,59 @@ public class Ejson {
private ReactApplicationContext reactContext;

private MMKV mmkv;

private boolean initializationAttempted = false;

private String TOKEN_KEY = "reactnativemeteor_usertoken-";

public Ejson() {
AppLifecycleFacade facade = AppLifecycleFacadeHolder.get();
if (facade != null && facade.getRunningReactContext() instanceof ReactApplicationContext) {
this.reactContext = (ReactApplicationContext) facade.getRunningReactContext();
// Don't initialize MMKV in constructor - use lazy initialization instead
}

/**
* Lazily initialize MMKV when first needed.
*
* NOTE: MMKV requires ReactApplicationContext (not regular Context) because SecureKeystore
* needs access to React-specific keystore resources. This means MMKV cannot be initialized
* before React Native starts.
*/
private void ensureMMKVInitialized() {
if (initializationAttempted) {
return;
}

// Only initialize MMKV if we have a valid React context
if (this.reactContext != null) {

initializationAttempted = true;

// Try to get ReactApplicationContext from available sources
if (this.reactContext == null) {
AppLifecycleFacade facade = AppLifecycleFacadeHolder.get();
if (facade != null) {
Object runningContext = facade.getRunningReactContext();
if (runningContext instanceof ReactApplicationContext) {
this.reactContext = (ReactApplicationContext) runningContext;
}
}

if (this.reactContext == null) {
this.reactContext = CustomPushNotification.reactApplicationContext;
}
}

// Initialize MMKV if context is available
if (this.reactContext != null && mmkv == null) {
try {
// Start MMKV container
MMKV.initialize(this.reactContext);
SecureKeystore secureKeystore = new SecureKeystore(this.reactContext);

// https://github.com/ammarahm-ed/react-native-mmkv-storage/blob/master/src/loader.js#L31
// Alias format from react-native-mmkv-storage
String alias = Utils.toHex("com.MMKV.default");

// Retrieve container password
String password = secureKeystore.getSecureKey(alias);
mmkv = MMKV.mmkvWithID("default", MMKV.SINGLE_PROCESS_MODE, password);
} catch (Exception e) {
Log.e("Ejson", "Failed to initialize MMKV: " + e.getMessage());
Log.e(TAG, "Failed to initialize MMKV", e);
mmkv = null;
}
} else {
Log.w("Ejson", "React context is null, MMKV will not be initialized");
mmkv = null;
} else if (this.reactContext == null) {
Log.w(TAG, "Cannot initialize MMKV: ReactApplicationContext not available");
}
}

Expand All @@ -85,22 +114,87 @@ public String getAvatarUri() {
}

public String token() {
ensureMMKVInitialized();
String userId = userId();
if (mmkv != null && userId != null) {
return mmkv.decodeString(TOKEN_KEY.concat(userId));

if (mmkv == null) {
Log.e(TAG, "token() called but MMKV is null");
return "";
}

if (userId == null || userId.isEmpty()) {
Log.w(TAG, "token() called but userId is null or empty");
return "";
}

String key = TOKEN_KEY.concat(userId);
if (BuildConfig.DEBUG) {
Log.d(TAG, "Looking up token with key: " + key);
}
return "";

String token = mmkv.decodeString(key);

if (token == null || token.isEmpty()) {
Log.w(TAG, "No token found in MMKV for userId");
} else if (BuildConfig.DEBUG) {
Log.d(TAG, "Successfully retrieved token from MMKV");
}

return token != null ? token : "";
}

public String userId() {
ensureMMKVInitialized();
String serverURL = serverURL();
if (mmkv != null && serverURL != null) {
return mmkv.decodeString(TOKEN_KEY.concat(serverURL));
String key = TOKEN_KEY.concat(serverURL);

if (mmkv == null) {
Log.e(TAG, "userId() called but MMKV is null");
return "";
}
return "";

if (serverURL == null) {
Log.e(TAG, "userId() called but serverURL is null");
return "";
}

if (BuildConfig.DEBUG) {
Log.d(TAG, "Looking up userId with key: " + key);
}

String userId = mmkv.decodeString(key);

if (userId == null || userId.isEmpty()) {
Log.w(TAG, "No userId found in MMKV for server: " + sanitizeUrl(serverURL));

// Only list keys in debug builds for diagnostics
if (BuildConfig.DEBUG) {
try {
String[] allKeys = mmkv.allKeys();
if (allKeys != null && allKeys.length > 0) {
Log.d(TAG, "Available MMKV keys count: " + allKeys.length);
// Log only keys that match the TOKEN_KEY pattern for security
for (String k : allKeys) {
if (k != null && k.startsWith("reactnativemeteor_usertoken")) {
Log.d(TAG, "Found auth key: " + k);
}
}
} else {
Log.w(TAG, "MMKV has no keys stored");
}
} catch (Exception e) {
Log.e(TAG, "Error listing MMKV keys", e);
}
}
} else if (BuildConfig.DEBUG) {
Log.d(TAG, "Successfully retrieved userId from MMKV");
}

return userId != null ? userId : "";
}

public String privateKey() {
ensureMMKVInitialized();
String serverURL = serverURL();
if (mmkv != null && serverURL != null) {
return mmkv.decodeString(serverURL.concat("-RC_E2E_PRIVATE_KEY"));
Expand All @@ -116,13 +210,41 @@ public String serverURL() {
return url;
}

public static class Sender {
String username;
static class Sender {
String _id;
String username;
String name;
}

public static class Content {
String ciphertext;
static class Content {
String algorithm;
String ciphertext;
String kid;
String iv;
}
}

/**
* Sanitize URL for logging by removing sensitive information
* @param url The URL to sanitize
* @return Sanitized URL showing only the protocol and host
*/
private String sanitizeUrl(String url) {
if (url == null) {
return "[null]";
}
try {
// Simple sanitization - just show protocol and host
if (url.startsWith("http://") || url.startsWith("https://")) {
int protocolEnd = url.indexOf("://") + 3;
int pathStart = url.indexOf("/", protocolEnd);
if (pathStart != -1) {
return url.substring(0, pathStart);
}
return url;
}
} catch (Exception e) {
// If parsing fails, just return a generic placeholder
}
return "[url]";
}
}
Loading
Loading