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
1 change: 1 addition & 0 deletions __mocks__/react-native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const createEventEmitterMock = () => vi.fn(() => ({ remove: vi.fn() }));
const mockRNOneSignal: Record<string, Mock> = {
initialize: vi.fn(),
login: vi.fn(),
loginWithProfile: vi.fn(),
logout: vi.fn(),
setPrivacyConsentRequired: vi.fn(),
setPrivacyConsentGiven: vi.fn(),
Expand Down
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def safeFlagGet(envProp, gradleProp) {
return normalizedValue != null && (normalizedValue.equalsIgnoreCase('true') || normalizedValue == '1')
}

def oneSignalVersion = '5.10.2'
def oneSignalVersion = '5.11.0'
def oneSignalDisableLocation = safeFlagGet('ONESIGNAL_DISABLE_LOCATION', 'onesignal.disableLocation')

android {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,12 @@ of this software and associated documentation files (the "Software"), to deal
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.onesignal.Continue;
import com.onesignal.LoginData;
import com.onesignal.OneSignal;
import com.onesignal.OneSignalResult;
import com.onesignal.common.OneSignalWrapper;
import com.onesignal.debug.LogLevel;
import com.onesignal.debug.internal.logging.Logging;
Expand All @@ -70,6 +73,12 @@ of this software and associated documentation files (the "Software"), to deal
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import kotlin.Result;
import kotlin.coroutines.Continuation;
import kotlin.coroutines.CoroutineContext;
import kotlin.coroutines.EmptyCoroutineContext;
import kotlin.coroutines.intrinsics.IntrinsicsKt;
import org.json.JSONException;

public class RNOneSignal extends NativeOneSignalSpec
Expand Down Expand Up @@ -605,6 +614,77 @@ public void login(String externalUserId) {
OneSignal.login(externalUserId);
}

@Override
public void loginWithProfile(String externalId, ReadableMap profile, @Nullable String jwt, Promise promise) {
AtomicBoolean settled = new AtomicBoolean(false);
Continuation<OneSignalResult<LoginData>> continuation = new Continuation<OneSignalResult<LoginData>>() {
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}

@Override
public void resumeWith(Object result) {
settleLoginWithProfile(promise, settled, result);
}
};

Object returned;
try {
returned = OneSignal.login(externalId, RNUtils.convertReadableMapToUserProfile(profile), jwt, continuation);
} catch (Throwable t) {
settleLoginWithProfile(promise, settled, new Result.Failure(t));
return;
}
// A non-suspending call returns its result here and never invokes resumeWith.
if (returned != IntrinsicsKt.getCOROUTINE_SUSPENDED()) {
settleLoginWithProfile(promise, settled, returned);
}
}

private static void settleLoginWithProfile(Promise promise, AtomicBoolean settled, Object result) {
if (!settled.compareAndSet(false, true)) return;

WritableMap payload;
try {
if (result instanceof OneSignalResult) {
payload = RNUtils.convertMapToWritableMap(((OneSignalResult<?>) result).toMap());
} else {
Throwable error = result instanceof Result.Failure ? ((Result.Failure) result).exception : null;
if (error != null) Logging.error("loginWithProfile failed", error);
payload = loginWithProfileFailure(
error != null ? describe(error) : "Unexpected loginWithProfile result");
}
} catch (Throwable t) {
Logging.error("Failed to serialize loginWithProfile result", t);
payload = loginWithProfileFailure(describe(t));
}
promise.resolve(payload);
}

private static String describe(Throwable t) {
return t.getMessage() != null ? t.getMessage() : t.toString();
}

private static WritableMap loginWithProfileFailure(String message) {
WritableMap error = Arguments.createMap();
error.putString("code", "UNKNOWN");
error.putString("source", "CLIENT");
error.putNull("backendCode");
error.putNull("httpStatus");
error.putString("message", message);
error.putNull("meta");

WritableArray errors = Arguments.createArray();
errors.pushMap(error);

WritableMap payload = Arguments.createMap();
payload.putBoolean("success", false);
payload.putNull("data");
payload.putArray("error", errors);
return payload;
}

@Override
public void logout() {
OneSignal.logout();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.facebook.react.bridge.ReadableType;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.onesignal.OneSignalUserProfile;
import com.onesignal.debug.internal.logging.Logging;
import com.onesignal.inAppMessages.IInAppMessage;
import com.onesignal.inAppMessages.IInAppMessageClickEvent;
Expand Down Expand Up @@ -291,6 +292,84 @@ public static HashMap<String, String> convertReadableMapIntoStringMap(ReadableMa
return stringMap;
}

public static OneSignalUserProfile convertReadableMapToUserProfile(ReadableMap profile) {
String email = null;
String sms = null;
Map<String, String> tags = new HashMap<>();
Map<String, String> aliases = new HashMap<>();
if (profile != null) {
if (profile.hasKey("email") && profile.getType("email") == ReadableType.String) {
email = profile.getString("email");
}
if (profile.hasKey("sms") && profile.getType("sms") == ReadableType.String) {
sms = profile.getString("sms");
}
if (profile.hasKey("tags") && profile.getType("tags") == ReadableType.Map) {
tags = convertReadableMapIntoStringMap(profile.getMap("tags"));
}
if (profile.hasKey("aliases") && profile.getType("aliases") == ReadableType.Map) {
aliases = convertReadableMapIntoStringMap(profile.getMap("aliases"));
}
}
return OneSignalUserProfile.builder()
.setEmail(email)
.setSms(sms)
.setTags(tags)
.setAliases(aliases)
.build();
}

public static WritableMap convertMapToWritableMap(Map<?, ?> map) {
WritableMap writableMap = Arguments.createMap();
for (Map.Entry<?, ?> entry : map.entrySet()) {
String key = String.valueOf(entry.getKey());
Object value = entry.getValue();

if (value == null) {
writableMap.putNull(key);
} else if (value instanceof String) {
writableMap.putString(key, (String) value);
} else if (value instanceof Boolean) {
writableMap.putBoolean(key, (Boolean) value);
} else if (value instanceof Integer) {
writableMap.putInt(key, (Integer) value);
} else if (value instanceof Number) {
writableMap.putDouble(key, ((Number) value).doubleValue());
} else if (value instanceof Map) {
writableMap.putMap(key, convertMapToWritableMap((Map<?, ?>) value));
} else if (value instanceof Iterable) {
writableMap.putArray(key, convertIterableToWritableArray((Iterable<?>) value));
} else {
writableMap.putString(key, String.valueOf(value));
}
}
return writableMap;
}

private static WritableArray convertIterableToWritableArray(Iterable<?> items) {
WritableArray writableArray = Arguments.createArray();
for (Object item : items) {
if (item == null) {
writableArray.pushNull();
} else if (item instanceof String) {
writableArray.pushString((String) item);
} else if (item instanceof Boolean) {
writableArray.pushBoolean((Boolean) item);
} else if (item instanceof Integer) {
writableArray.pushInt((Integer) item);
} else if (item instanceof Number) {
writableArray.pushDouble(((Number) item).doubleValue());
} else if (item instanceof Map) {
writableArray.pushMap(convertMapToWritableMap((Map<?, ?>) item));
} else if (item instanceof Iterable) {
writableArray.pushArray(convertIterableToWritableArray((Iterable<?>) item));
} else {
writableArray.pushString(String.valueOf(item));
}
}
return writableArray;
}

public static HashMap<String, Object> convertPermissionToMap(boolean granted) {
HashMap<String, Object> hash = new HashMap<>();

Expand Down
122 changes: 122 additions & 0 deletions examples/demo/src/components/modals/LoginWithProfileModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import React, { useState } from 'react';
import {
Modal,
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
KeyboardAvoidingView,
Platform,
} from 'react-native';

import { AppColors, AppDialogStyles, AppInputProps } from '../../theme';

interface Props {
visible: boolean;
onConfirm: (externalUserId: string, email: string, sms: string, jwt: string) => void;
onClose: () => void;
}

export default function LoginWithProfileModal({ visible, onConfirm, onClose }: Props) {
const [externalUserId, setExternalUserId] = useState('');
const [email, setEmail] = useState('');
const [sms, setSms] = useState('');
const [jwt, setJwt] = useState('');

const canSubmit = !!externalUserId.trim();

const handleConfirm = () => {
if (!canSubmit) {
return;
}
onConfirm(externalUserId.trim(), email.trim(), sms.trim(), jwt.trim());
handleClose();
};

const handleClose = () => {
setExternalUserId('');
setEmail('');
setSms('');
setJwt('');
onClose();
};

return (
<Modal visible={visible} transparent animationType="fade" onRequestClose={handleClose}>
<KeyboardAvoidingView
style={AppDialogStyles.backdrop}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
<View style={AppDialogStyles.container}>
<Text style={AppDialogStyles.title}>Login With Profile</Text>
<TextInput
style={[AppDialogStyles.input, styles.inputSpacing]}
placeholder="External User Id"
placeholderTextColor={AppColors.osGrey600}
value={externalUserId}
onChangeText={setExternalUserId}
autoFocus
{...AppInputProps}
testID="composite_login_user_id_input"
/>
<TextInput
style={[AppDialogStyles.input, styles.inputSpacing]}
placeholder="Email (optional)"
placeholderTextColor={AppColors.osGrey600}
value={email}
onChangeText={setEmail}
keyboardType="email-address"
{...AppInputProps}
testID="composite_login_email_input"
/>
<TextInput
style={[AppDialogStyles.input, styles.inputSpacing]}
placeholder="SMS (optional)"
placeholderTextColor={AppColors.osGrey600}
value={sms}
onChangeText={setSms}
keyboardType="phone-pad"
{...AppInputProps}
testID="composite_login_sms_input"
/>
<TextInput
style={[AppDialogStyles.input, styles.inputSpacing]}
placeholder="JWT Token (optional)"
placeholderTextColor={AppColors.osGrey600}
value={jwt}
onChangeText={setJwt}
{...AppInputProps}
testID="composite_login_jwt_input"
/>
<View style={AppDialogStyles.actions}>
<TouchableOpacity style={AppDialogStyles.actionBtn} onPress={handleClose}>
<Text style={AppDialogStyles.actionText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={AppDialogStyles.actionBtn}
onPress={handleConfirm}
disabled={!canSubmit}
testID="composite_login_confirm_button"
>
<Text
style={[
AppDialogStyles.actionText,
!canSubmit && AppDialogStyles.actionTextDisabled,
]}
>
Login
</Text>
</TouchableOpacity>
</View>
</View>
</KeyboardAvoidingView>
</Modal>
);
}

const styles = StyleSheet.create({
inputSpacing: {
marginBottom: 12,
},
});
Loading
Loading