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
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,10 @@ public void addOutcomeWithValue(String name, double value) {

@Override
public void login(String externalUserId) {
if (externalUserId == null || externalUserId.isEmpty()) {
Logging.error("login called with a null or empty externalUserId", null);
return;
}
OneSignal.login(externalUserId);
}

Expand Down
5 changes: 5 additions & 0 deletions ios/RCTOneSignal/RCTOneSignalEventEmitter.mm
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ + (void)sendEventWithName:(NSString *)name withBody:(NSDictionary *)body {
}

RCT_EXPORT_METHOD(login : (NSString *)externalId) {
if (externalId == nil || [externalId length] == 0) {
[OneSignalLog onesignalLog:ONE_S_LL_ERROR
message:@"login called with a nil or empty externalId"];
return;
}
[OneSignal login:externalId];
}

Expand Down
94 changes: 93 additions & 1 deletion src/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import { beforeEach, describe, expect, test, vi, type MockInstance } from 'vite-
import { IOS_NULL_SENTINEL } from './constants/internal';
import {
encodeNullsForIOS,
hasMissingEntries,
hasMissingItems,
isBoolean,
isInteger,
isNativeModuleLoaded,
isMissing,
isObjectSerializable,
isValidCallback,
} from './helpers';
Expand Down Expand Up @@ -55,7 +60,7 @@ describe('helpers', () => {
expect(result).toBe(false);
expect(errorSpy).toHaveBeenCalledTimes(1);
expect(errorSpy).toHaveBeenCalledWith(
'Could not load RNOneSignal native module. Make sure native dependencies are properly linked.',
'[OneSignal] Could not load RNOneSignal native module. Make sure native dependencies are properly linked.',
);
},
);
Expand All @@ -66,6 +71,93 @@ describe('helpers', () => {
});
});

describe('isBoolean', () => {
test.each([true, false])('should accept %s', (value) => {
expect(isBoolean(value, 'api: flag')).toBe(true);
expect(errorSpy).not.toHaveBeenCalled();
});

test.each([undefined, null, 'true', 0, {}])('should reject %s', (value) => {
expect(isBoolean(value, 'api: flag')).toBe(false);
expect(errorSpy).toHaveBeenCalledWith('[OneSignal] api: flag must be a boolean');
});
});

describe('isInteger', () => {
test.each([0, 5, -3])('should accept %s', (value) => {
expect(isInteger(value, 'api: id')).toBe(true);
expect(errorSpy).not.toHaveBeenCalled();
});

test.each([undefined, null, '1', 1.5, NaN, Infinity])('should reject %s', (value) => {
expect(isInteger(value, 'api: id')).toBe(false);
expect(errorSpy).toHaveBeenCalledWith('[OneSignal] api: id must be an integer');
});
});

describe('isMissing', () => {
test.each([
{ description: 'a non-empty string', value: 'id', expected: false },
{ description: 'a whitespace string', value: ' ', expected: false },
{ description: 'an empty string', value: '', expected: true },
{ description: 'null', value: null, expected: true },
{ description: 'undefined', value: undefined, expected: true },
{ description: 'a number', value: 1, expected: true },
{ description: 'a boolean', value: true, expected: true },
])(
'should return $expected for $description',
({ value, expected }: { description: string; value: unknown; expected: boolean }) => {
expect(isMissing(value, 'login: externalId')).toBe(expected);
},
);
});

describe('hasMissingEntries', () => {
test.each([
{ description: 'null', values: null, allowEmptyValue: false, expected: true },
{ description: 'an array', values: ['x'], allowEmptyValue: false, expected: true },
{ description: 'an empty key', values: { '': 'id' }, allowEmptyValue: false, expected: true },
{ description: 'an empty value', values: { a: '' }, allowEmptyValue: false, expected: true },
{ description: 'a numeric value', values: { a: 5 }, allowEmptyValue: false, expected: true },
{
description: 'an allowed empty value',
values: { a: '' },
allowEmptyValue: true,
expected: false,
},
{
description: 'an allowed numeric value',
values: { a: 5 },
allowEmptyValue: true,
expected: false,
},
{ description: 'a null value', values: { a: null }, allowEmptyValue: true, expected: true },
{
description: 'valid entries',
values: { a: 'id' },
allowEmptyValue: false,
expected: false,
},
])('should return $expected for $description', ({ values, allowEmptyValue, expected }) => {
expect(
hasMissingEntries(values as Record<string, unknown> | null, 'addAliases', allowEmptyValue),
).toBe(expected);
});
});

describe('hasMissingItems', () => {
test.each([
{ description: 'null', values: null, expected: true },
{ description: 'a string', values: 'key', expected: true },
{ description: 'an empty item', values: ['a', ''], expected: true },
{ description: 'a non-string item', values: ['a', 1], expected: true },
{ description: 'valid items', values: ['a', 'b'], expected: false },
{ description: 'an empty array', values: [], expected: false },
])('should return $expected for $description', ({ values, expected }) => {
expect(hasMissingItems(values, 'removeTags', 'key')).toBe(expected);
});
});

describe('isObjectSerializable', () => {
test.each([
{ description: 'an empty object', value: {} },
Expand Down
53 changes: 52 additions & 1 deletion src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export function isValidCallback(handler: Function) {
export function isNativeModuleLoaded(module: object | null | undefined): boolean {
if (module == null) {
console.error(
'Could not load RNOneSignal native module. Make sure native dependencies are properly linked.',
'[OneSignal] Could not load RNOneSignal native module. Make sure native dependencies are properly linked.',
);

return false;
Expand All @@ -18,6 +18,57 @@ export function isNativeModuleLoaded(module: object | null | undefined): boolean
return true;
}

export function isMissing(value: unknown, api: string): boolean {
if (typeof value === 'string' && value.length > 0) return false;
console.error(`[OneSignal] ${api} is required`);
return true;
}

export function isBoolean(value: unknown, api: string): value is boolean {
if (typeof value === 'boolean') return true;
console.error(`[OneSignal] ${api} must be a boolean`);
return false;
}

export function isInteger(value: unknown, api: string): value is number {
if (Number.isInteger(value)) return true;
console.error(`[OneSignal] ${api} must be an integer`);
return false;
}

export function isObject(value: unknown, api: string): value is Record<string, unknown> {
if (typeof value === 'object' && value !== null && !Array.isArray(value)) return true;
console.error(`[OneSignal] ${api} must be an object`);
return false;
}

export function isFunction(value: unknown, api: string): value is Function {
if (typeof value === 'function') return true;
console.error(`[OneSignal] ${api} must be a function`);
return false;
}

export function hasMissingEntries(
values: Record<string, unknown> | null | undefined,
api: string,
allowEmptyValue = false,
): boolean {
if (!isObject(values, `${api}: argument`)) return true;
return Object.entries(values).some(([key, item]) => {
if (isMissing(key, `${api}: key`)) return true;
if (!allowEmptyValue) return isMissing(item, `${api}: value`);
return item == null && isMissing(item, `${api}: value`);
});
}

export function hasMissingItems(values: unknown, api: string, item: string): boolean {
if (!Array.isArray(values)) {
console.error(`[OneSignal] ${api}: ${item}s must be an array of strings`);
return true;
}
return values.some((value) => isMissing(value, `${api}: ${item}`));
}

/**
* Returns true if the value is a JSON-serializable object.
*/
Expand Down
Loading
Loading