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
2 changes: 2 additions & 0 deletions __mocks__/react-native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ const mockRNOneSignal = {
addPushSubscriptionObserver: vi.fn(),
getPushSubscriptionId: vi.fn(),
getPushSubscriptionToken: vi.fn(),
waitForPushSubscriptionIdAsync: vi.fn(),
waitForPushSubscriptionTokenAsync: vi.fn(),
getOptedIn: vi.fn(),
optOut: vi.fn(),
optIn: vi.fn(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,102 @@ public void getPushSubscriptionToken(Promise promise) {
}
}

@ReactMethod
public void waitForPushSubscriptionIdAsync(final int timeoutMs, final Promise promise) {
IPushSubscription pushSubscription = OneSignal.getUser().getPushSubscription();
String currentId = pushSubscription.getId();

// If ID already exists, resolve immediately
if (currentId != null && !currentId.isEmpty()) {
promise.resolve(currentId);
return;
}

// Create a one-time observer
final IPushSubscriptionObserver observer = new IPushSubscriptionObserver() {
private boolean isResolved = false;
private final android.os.Handler handler = new android.os.Handler(android.os.Looper.getMainLooper());
private Runnable timeoutRunnable;

{
// Set up timeout
timeoutRunnable = new Runnable() {
@Override
public void run() {
if (!isResolved) {
isResolved = true;
OneSignal.getUser().getPushSubscription().removeObserver(this);
promise.resolve(null);
}
}
};
handler.postDelayed(timeoutRunnable, timeoutMs);
}

@Override
public void onPushSubscriptionChange(PushSubscriptionChangedState state) {
String newId = state.getCurrent().getId();
if (newId != null && !newId.isEmpty() && !isResolved) {
isResolved = true;
handler.removeCallbacks(timeoutRunnable);
OneSignal.getUser().getPushSubscription().removeObserver(this);
promise.resolve(newId);
}
}
};

// Add the observer
pushSubscription.addObserver(observer);
}

@ReactMethod
public void waitForPushSubscriptionTokenAsync(final int timeoutMs, final Promise promise) {
IPushSubscription pushSubscription = OneSignal.getUser().getPushSubscription();
String currentToken = pushSubscription.getToken();

// If token already exists, resolve immediately
if (currentToken != null && !currentToken.isEmpty()) {
promise.resolve(currentToken);
return;
}

// Create a one-time observer
final IPushSubscriptionObserver observer = new IPushSubscriptionObserver() {
private boolean isResolved = false;
private final android.os.Handler handler = new android.os.Handler(android.os.Looper.getMainLooper());
private Runnable timeoutRunnable;

{
// Set up timeout
timeoutRunnable = new Runnable() {
@Override
public void run() {
if (!isResolved) {
isResolved = true;
OneSignal.getUser().getPushSubscription().removeObserver(this);
promise.resolve(null);
}
}
};
handler.postDelayed(timeoutRunnable, timeoutMs);
}

@Override
public void onPushSubscriptionChange(PushSubscriptionChangedState state) {
String newToken = state.getCurrent().getToken();
if (newToken != null && !newToken.isEmpty() && !isResolved) {
isResolved = true;
handler.removeCallbacks(timeoutRunnable);
OneSignal.getUser().getPushSubscription().removeObserver(this);
promise.resolve(newToken);
}
}
};

// Add the observer
pushSubscription.addObserver(observer);
}

@ReactMethod
public void getOptedIn(Promise promise) {
IPushSubscription pushSubscription = OneSignal.getUser().getPushSubscription();
Expand Down
96 changes: 84 additions & 12 deletions ios/RCTOneSignal/RCTOneSignalEventEmitter.m
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ - (void)stopObserving {
} else if (error.userInfo[@"returned"]) {
return @[error.userInfo[@"returned"]];
}

return @[error.localizedDescription];
}

Expand Down Expand Up @@ -112,7 +112,7 @@ + (void)sendEventWithName:(NSString *)name withBody:(NSDictionary *)body {
[OneSignal logout];
}

RCT_EXPORT_METHOD(enterLiveActivity:(NSString *)activityId
RCT_EXPORT_METHOD(enterLiveActivity:(NSString *)activityId
withToken:(NSString *)token
withResponse:(RCTResponseSenderBlock)callback) {
[OneSignal.LiveActivities enter:activityId withToken:token withSuccess:^(NSDictionary *result) {
Expand All @@ -131,7 +131,7 @@ + (void)sendEventWithName:(NSString *)name withBody:(NSDictionary *)body {
}];
}

RCT_EXPORT_METHOD(setPushToStartToken:(NSString *)activityType
RCT_EXPORT_METHOD(setPushToStartToken:(NSString *)activityType
withToken:(NSString *)token) {
#if !TARGET_OS_MACCATALYST
NSError* err=nil;
Expand Down Expand Up @@ -209,7 +209,7 @@ + (void)sendEventWithName:(NSString *)name withBody:(NSDictionary *)body {
}

// OneSignal.InAppMessages namespace methods
RCT_REMAP_METHOD(getPaused,
RCT_REMAP_METHOD(getPaused,
getPausedResolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
resolve(@([OneSignal.InAppMessages paused]));
Expand Down Expand Up @@ -269,13 +269,13 @@ + (void)sendEventWithName:(NSString *)name withBody:(NSDictionary *)body {
}

// OneSignal.Notifications namespace methods
RCT_REMAP_METHOD(hasNotificationPermission,
RCT_REMAP_METHOD(hasNotificationPermission,
hasNotificationPermissionResolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
resolve(@([OneSignal.Notifications permission]));
}

RCT_REMAP_METHOD(canRequestNotificationPermission,
RCT_REMAP_METHOD(canRequestNotificationPermission,
canRequestNotificationPermissionResolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
resolve(@([OneSignal.Notifications canRequestPermission]));
Expand Down Expand Up @@ -408,7 +408,7 @@ + (void)sendEventWithName:(NSString *)name withBody:(NSDictionary *)body {
}

RCT_EXPORT_METHOD(removeSms:(NSString *)smsNumber) {
[OneSignal.User removeSms:smsNumber];
[OneSignal.User removeSms:smsNumber];
}

RCT_EXPORT_METHOD(addTag:(NSString *)key value:(id)value) {
Expand Down Expand Up @@ -436,7 +436,7 @@ + (void)sendEventWithName:(NSString *)name withBody:(NSDictionary *)body {
getOnesignalIdResolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
NSString *onesignalId = OneSignal.User.onesignalId;

if (onesignalId == nil || [onesignalId length] == 0) {
resolve([NSNull null]); // Resolve with null if nil or empty
} else {
Expand All @@ -448,7 +448,7 @@ + (void)sendEventWithName:(NSString *)name withBody:(NSDictionary *)body {
getExternalIdResolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
NSString *externalId = OneSignal.User.externalId;

if (externalId == nil || [externalId length] == 0) {
resolve([NSNull null]); // Resolve with null if nil or empty
} else {
Expand All @@ -474,13 +474,13 @@ + (void)sendEventWithName:(NSString *)name withBody:(NSDictionary *)body {


// OneSignal.User.pushSubscription namespace methods
RCT_REMAP_METHOD(getOptedIn,
RCT_REMAP_METHOD(getOptedIn,
getOptedInResolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
resolve(@(OneSignal.User.pushSubscription.optedIn));
}

RCT_REMAP_METHOD(getPushSubscriptionId,
RCT_REMAP_METHOD(getPushSubscriptionId,
getPushSubscriptionIdResolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
NSString *pushId = OneSignal.User.pushSubscription.id;
Expand All @@ -491,7 +491,7 @@ + (void)sendEventWithName:(NSString *)name withBody:(NSDictionary *)body {
}
}

RCT_REMAP_METHOD(getPushSubscriptionToken,
RCT_REMAP_METHOD(getPushSubscriptionToken,
getPushSubscriptionTokenResolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
NSString *token = OneSignal.User.pushSubscription.token;
Expand All @@ -502,6 +502,78 @@ + (void)sendEventWithName:(NSString *)name withBody:(NSDictionary *)body {
}
}

RCT_REMAP_METHOD(waitForPushSubscriptionIdAsync,
waitForPushSubscriptionIdWithTimeoutMs:(nonnull NSNumber *)timeoutMs
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
NSString *currentId = OneSignal.User.pushSubscription.id;

// If ID already exists, resolve immediately
if (currentId && ![currentId isEqualToString:@""]) {
resolve(currentId);
return;
}

__block BOOL isResolved = NO;
__block id<NSObject> observer = nil;

// Create observer
observer = [OneSignal.User.pushSubscription addObserver:^(OSPushSubscriptionChangedState * _Nonnull state) {
NSString *newId = state.current.id;
if (newId && ![newId isEqualToString:@""] && !isResolved) {
isResolved = YES;
[OneSignal.User.pushSubscription removeObserver:observer];
resolve(newId);
}
}];

// Set up timeout
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)([timeoutMs integerValue] * NSEC_PER_MSEC)),
dispatch_get_main_queue(), ^{
if (!isResolved) {
isResolved = YES;
[OneSignal.User.pushSubscription removeObserver:observer];
resolve([NSNull null]);
}
});
}

RCT_REMAP_METHOD(waitForPushSubscriptionTokenAsync,
waitForPushSubscriptionTokenWithTimeoutMs:(nonnull NSNumber *)timeoutMs
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
NSString *currentToken = OneSignal.User.pushSubscription.token;

// If token already exists, resolve immediately
if (currentToken && ![currentToken isEqualToString:@""]) {
resolve(currentToken);
return;
}

__block BOOL isResolved = NO;
__block id<NSObject> observer = nil;

// Create observer
observer = [OneSignal.User.pushSubscription addObserver:^(OSPushSubscriptionChangedState * _Nonnull state) {
NSString *newToken = state.current.token;
if (newToken && ![newToken isEqualToString:@""] && !isResolved) {
isResolved = YES;
[OneSignal.User.pushSubscription removeObserver:observer];
resolve(newToken);
}
}];

// Set up timeout
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)([timeoutMs integerValue] * NSEC_PER_MSEC)),
dispatch_get_main_queue(), ^{
if (!isResolved) {
isResolved = YES;
[OneSignal.User.pushSubscription removeObserver:observer];
resolve([NSNull null]);
}
});
}

RCT_EXPORT_METHOD(optIn) {
[OneSignal.User.pushSubscription optIn];
}
Expand Down
86 changes: 86 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,48 @@ describe('OneSignal', () => {
OneSignal.User.pushSubscription.getIdAsync(),
).rejects.toThrow('OneSignal native module not loaded');
});

test('should wait for subscription id using native method', async () => {
// Mock the native wait method to resolve with ID after delay
mockRNOneSignal.waitForPushSubscriptionIdAsync.mockResolvedValue(
PUSH_ID,
);

const result = await OneSignal.User.pushSubscription.getIdAsync({
timeout: 5000,
});

expect(result).toBe(PUSH_ID);
expect(
mockRNOneSignal.waitForPushSubscriptionIdAsync,
).toHaveBeenCalledWith(5000);
});

test('should return null if id not available after timeout', async () => {
mockRNOneSignal.waitForPushSubscriptionIdAsync.mockResolvedValue(null);

const result = await OneSignal.User.pushSubscription.getIdAsync({
timeout: 1000,
});

expect(result).toBeNull();
expect(
mockRNOneSignal.waitForPushSubscriptionIdAsync,
).toHaveBeenCalledWith(1000);
});

test('should use default timeout if not specified', async () => {
mockRNOneSignal.waitForPushSubscriptionIdAsync.mockResolvedValue(
PUSH_ID,
);

const result = await OneSignal.User.pushSubscription.getIdAsync();

expect(result).toBe(PUSH_ID);
expect(
mockRNOneSignal.waitForPushSubscriptionIdAsync,
).toHaveBeenCalledWith(5000); // Default timeout
});
});

describe('getPushSubscriptionToken (deprecated)', () => {
Expand Down Expand Up @@ -502,6 +544,50 @@ describe('OneSignal', () => {
OneSignal.User.pushSubscription.getTokenAsync(),
).rejects.toThrow('OneSignal native module not loaded');
});

test('should wait for subscription token using native method', async () => {
// Mock the native wait method to resolve with token
vi.mocked(
mockRNOneSignal.waitForPushSubscriptionTokenAsync,
).mockResolvedValue(PUSH_TOKEN);

const result = await OneSignal.User.pushSubscription.getTokenAsync({
timeout: 5000,
});

expect(result).toBe(PUSH_TOKEN);
expect(
mockRNOneSignal.waitForPushSubscriptionTokenAsync,
).toHaveBeenCalledWith(5000);
});

test('should return null if token not available after timeout', async () => {
vi.mocked(
mockRNOneSignal.waitForPushSubscriptionTokenAsync,
).mockResolvedValue(null);

const result = await OneSignal.User.pushSubscription.getTokenAsync({
timeout: 1000,
});

expect(result).toBeNull();
expect(
mockRNOneSignal.waitForPushSubscriptionTokenAsync,
).toHaveBeenCalledWith(1000);
});

test('should use default timeout if not specified', async () => {
vi.mocked(
mockRNOneSignal.waitForPushSubscriptionTokenAsync,
).mockResolvedValue(PUSH_TOKEN);

const result = await OneSignal.User.pushSubscription.getTokenAsync();

expect(result).toBe(PUSH_TOKEN);
expect(
mockRNOneSignal.waitForPushSubscriptionTokenAsync,
).toHaveBeenCalledWith(5000); // Default timeout
});
});

describe('getOptedIn (deprecated)', () => {
Expand Down
Loading
Loading