Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Klarna SignIn Implementation #299

Open
wants to merge 38 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
f617a56
MSDK-892 Added KlarnaSignIn module specification
jorgepalac1o Feb 20, 2025
8031d7c
MSDK-892 Added SignIn screen to the RN test app
jorgepalac1o Feb 20, 2025
cde4a48
MSDK-892 Added KlarnaSignInManager native iOS class and renamed the m…
jorgepalac1o Feb 21, 2025
f3d9261
MSDK-892 Added module name to match turbo generated one, also impleme…
jorgepalac1o Feb 25, 2025
5b151b7
MSDK-892 Added new architecture validations to make turbo native modu…
jorgepalac1o Feb 25, 2025
85c9cf1
MSDK-892 Fixed old architecture build errors
jorgepalac1o Feb 25, 2025
938c60f
MSDK-892 KlarnaSignInEventsHandler class created to centralize common…
jorgepalac1o Feb 26, 2025
b55bf32
MSDK-892 Fixed new architecture build errors. Library module hidden b…
jorgepalac1o Feb 26, 2025
0dffaba
MSDK-892 Added KlarnaSignInManager native module for Android in new a…
jorgepalac1o Feb 26, 2025
47b200a
MSDK-892 Fixed sign in sdk initialization by running the init from th…
jorgepalac1o Feb 27, 2025
423516e
MSDK-892 Moved KlarnaSignInManager to newArch folder.
jorgepalac1o Feb 27, 2025
622e045
MSDK-892 Added old arch klarna sign in spec and implementation. Renam…
jorgepalac1o Feb 27, 2025
8b519e8
MSDK-892 Renamed implementation in android new arch to use the module…
jorgepalac1o Feb 27, 2025
ba03e47
MSDK-892 Renamed implementation in ios on both archs to use the modul…
jorgepalac1o Feb 27, 2025
a7f1a5f
MSDK-892 Code formatting after testing new arch
jorgepalac1o Feb 27, 2025
d8302e0
MSDK-892 Added KlarnaRedirectReceiverActivity to the test app manifes…
jorgepalac1o Feb 28, 2025
5c1ebaa
MSDK-892 Added new intent to manifest and removed validations from si…
jorgepalac1o Mar 3, 2025
e724471
MSDK-892 Fixed serialization of events in android signIn module, also…
jorgepalac1o Mar 3, 2025
88aaa3b
MSDK-892 Renamed manager to module to keep it consistent, updated wit…
jorgepalac1o Mar 5, 2025
3342b5a
MSDK-892 Fixed android failing tests
jorgepalac1o Mar 5, 2025
e0fd8e4
MSDK-892 Fixed ios failing tests
jorgepalac1o Mar 5, 2025
86dcf2d
PR comments
jorgepalac1o Mar 6, 2025
4bf65b2
MSDK-892 Removed intent, updated event handler methods to return all …
jorgepalac1o Mar 12, 2025
da2db9b
MSDK-892 Added enums for region and environment
jorgepalac1o Mar 12, 2025
bb1ec30
MSDK-892 Moved region and environment enums to type. Updated signIn r…
jorgepalac1o Mar 12, 2025
832eea2
MSDK-892 Updated signIn screen view in the RN test app
jorgepalac1o Mar 12, 2025
1bb59da
MSDK-892 Updated android native module with latest changes on the RN …
jorgepalac1o Mar 12, 2025
7e5bcff
MSDK-892 Updated android implementation in new architecture, fixed ev…
jorgepalac1o Mar 12, 2025
7f5cf1c
MSDK-892 Updated iOS implementation in Old Arch
jorgepalac1o Mar 12, 2025
525a4d0
MSDK-892 Updated iOS implementation in New Arch
jorgepalac1o Mar 12, 2025
1ae97fa
MSDK-892 Renamed module implementation files
jorgepalac1o Mar 12, 2025
5fb9bf3
MSDK-892 Renamed module implementation files on android
jorgepalac1o Mar 12, 2025
65f0580
MSDK-892 Added promise to module initialisation
jorgepalac1o Mar 19, 2025
2b058aa
MSDK-892 Replaced sdk list for hashMap, updated logs in the signIn te…
jorgepalac1o Mar 19, 2025
d37e309
MSDK-892 Updated newArch in android module spec
jorgepalac1o Mar 19, 2025
93be843
MSDK-892 Updated oldArch functions in iOS. Replaced array with dictio…
jorgepalac1o Mar 19, 2025
db95401
MSDK-892 Updated newArch in iOS module spec
jorgepalac1o Mar 19, 2025
42f8135
MSDK-892 Refactored error name
jorgepalac1o Mar 19, 2025
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
3 changes: 3 additions & 0 deletions TestApp/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import HomeScreen from './src/home/HomeScreen';
import PaymentsScreen from './src/payments/PaymentsScreen';
import StandaloneWebViewScreen from './src/standalonewebview/StandaloneWebViewScreen';
import KlarnaCheckoutScreen from './src/checkout/KlarnaCheckoutScreen';
import SignInScreen from './src/signIn/SignInScreen.tsx';

const Stack = createNativeStackNavigator<AppStackParamList>();

Expand All @@ -29,6 +30,7 @@ const AppStack = () => {
component={StandaloneWebViewScreen}
/>
<Stack.Screen name="KlarnaCheckout" component={KlarnaCheckoutScreen} />
<Stack.Screen name="SignIn" component={SignInScreen} />
</Stack.Navigator>
);
};
Expand All @@ -46,6 +48,7 @@ type AppStackParamList = {
Payments: undefined;
StandaloneWebView: undefined;
KlarnaCheckout: undefined;
SignIn: undefined;
};

export type {AppStackParamList};
18 changes: 17 additions & 1 deletion TestApp/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET" />

Expand All @@ -21,5 +22,20 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="com.klarna.mobile.sdk.activity.KlarnaRedirectReceiverActivity"
android:exported="true"
tools:node="replace">
<intent-filter>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data
android:host="siwk"
android:scheme="in-app-test" />
</intent-filter>
</activity>
</application>
</manifest>
14 changes: 14 additions & 0 deletions TestApp/src/home/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,20 @@ export default function HomeScreen() {
Klarna Checkout
</Text>
</View>
<View
style={{
backgroundColor: isDarkMode ? Colors.black : Colors.white,
}}>
<Text
style={styles.navMenuItem}
{...testProps('navKlarnaSignIn')}
onPress={() => {
console.log('Navigating to KlarnaSignIn');
navigation.navigate('SignIn');
}}>
Klarna Sign In
</Text>
</View>
</ScrollView>
);
}
106 changes: 106 additions & 0 deletions TestApp/src/signIn/SignInScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import React, {useEffect, useState} from 'react';
import {ScrollView, TextInput, Text, useColorScheme, View} from 'react-native';
import styles, {backgroundStyle} from '../common/ui/Styles';
import testProps from '../common/util/TestProps';
import Button from '../common/ui/view/Button.tsx';
import {KlarnaSignIn} from 'react-native-klarna-inapp-sdk';
import {KlarnaEnvironment} from '../../../src/types/common/KlarnaEnvironment.ts';
import {KlarnaRegion} from '../../../src/types/common/KlarnaRegion.ts';

export default function SignInScreen() {
const isDarkMode = useColorScheme() === 'dark';

const [clientId, setClientId] = useState('');
const [scope, setScope] = useState('');
const [market, setMarket] = useState('');
const [locale, setLocale] = useState('');
const [tokenizationId, setTokenizationId] = useState('');
const [event, setEvent] = useState<string>();
const [klarnaSignIn, setKlarnaSignIn] = useState<KlarnaSignIn | null>(null);

useEffect(() => {
KlarnaSignIn.createInstance({
environment: KlarnaEnvironment.Staging,
region: KlarnaRegion.EU,
returnUrl: 'in-app-test://siwk',
})
.then(instance => {
console.log('KlarnaSignIn instance created: ', instance);
setKlarnaSignIn(instance);
setEvent(
_ => 'KlarnaSignIn instance created: ' + JSON.stringify(instance),
);
})
.catch(e => {
console.error('KlarnaSignIn instance creation failed: ', e);
setEvent(
_ => 'KlarnaSignIn instance creation failed: ' + JSON.stringify(e),
);
});
}, []);

const onEvent = (...params: Array<string | boolean | null>) => {
console.log('onEvent', params);
setEvent(prevState =>
prevState
? `${prevState} ${params.join('\n ----- \n')}`
: params.join('\n ----- \n'),
);
};

const renderTextField = (
label: string,
value: string,
setValue: (text: string) => void,
) => {
return (
<View style={{marginBottom: 20, width: '80%'}}>

Check warning on line 57 in TestApp/src/signIn/SignInScreen.tsx

View workflow job for this annotation

GitHub Actions / lint

Inline style: { marginBottom: 20, width: '80%' }
<Text style={styles.title}>{label}</Text>
<TextInput
autoCapitalize="none"
style={styles.tokenInput}
value={value}
placeholder={`Enter ${label}`}
onChangeText={setValue}
{...testProps(`${label}Input`)}
/>
</View>
);
};

return (
<ScrollView
contentInsetAdjustmentBehavior="automatic"
style={backgroundStyle(styles.scrollView, isDarkMode)}>
<View style={styles.container}>
{renderTextField('Client ID', clientId, setClientId)}
{renderTextField('Scope', scope, setScope)}
{renderTextField('Market', market, setMarket)}
{renderTextField('Locale', locale, setLocale)}
{renderTextField('Tokenization ID', tokenizationId, setTokenizationId)}
</View>
<View style={styles.buttonsContainer}>
<Button
title="Sign In"
onPress={() => {
console.log(
'Klarna sign in with KlarnaMobileSDK should start now on the native side',
);
klarnaSignIn
?.signIn(clientId, scope, market, locale, tokenizationId)
.then(r => {
console.log('Sign in success with result: ', r);
onEvent('Sign in success with result: ', JSON.stringify(r));
})
.catch(e => {
console.error('Sign in failed with error: ', e);
onEvent('Sign in failed with error: ', JSON.stringify(e));
});
}}
/>
</View>
<Text style={styles.title}>"Events Log"</Text>
<Text style={styles.title}>{event}</Text>
</ScrollView>
);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package com.klarna.mobile.sdk.reactnative;

import android.app.Application;

import androidx.annotation.NonNull;

import com.facebook.react.ReactPackage;
Expand All @@ -10,17 +8,17 @@
import com.facebook.react.uimanager.ViewManager;
import com.klarna.mobile.sdk.reactnative.checkout.KlarnaCheckoutViewManager;
import com.klarna.mobile.sdk.reactnative.payments.KlarnaPaymentViewManager;
import com.klarna.mobile.sdk.reactnative.spec.RNKlarnaSignInModuleSpec;
import com.klarna.mobile.sdk.reactnative.standalonewebview.KlarnaStandaloneWebViewManager;

import java.util.Collections;
import java.util.List;

public class KlarnaMobileSDKPackage implements ReactPackage {

@NonNull
@Override
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
return Collections.emptyList();
return List.of(new RNKlarnaSignInModuleSpec(reactContext));
}

@NonNull
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.klarna.mobile.sdk.reactnative.signin;

import com.facebook.react.bridge.Promise;
import com.klarna.mobile.sdk.api.signin.KlarnaSignInSDK;

public class KlarnaSignInData {
String instanceId;
Promise promise;
KlarnaSignInSDK sdkInstance;

public KlarnaSignInData(String instanceId, KlarnaSignInSDK sdkInstance) {
this.instanceId = instanceId;
this.sdkInstance = sdkInstance;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package com.klarna.mobile.sdk.reactnative.signin;

import static com.klarna.mobile.sdk.reactnative.common.util.ParserUtil.gson;

import androidx.annotation.NonNull;

import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.klarna.mobile.sdk.KlarnaMobileSDKError;
import com.klarna.mobile.sdk.api.KlarnaEnvironment;
import com.klarna.mobile.sdk.api.KlarnaEventHandler;
import com.klarna.mobile.sdk.api.KlarnaProductEvent;
import com.klarna.mobile.sdk.api.KlarnaRegion;
import com.klarna.mobile.sdk.api.component.KlarnaComponent;
import com.klarna.mobile.sdk.api.signin.KlarnaSignInEvent;
import com.klarna.mobile.sdk.api.signin.KlarnaSignInSDK;
import com.klarna.mobile.sdk.reactnative.common.util.ArgumentsUtil;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public class KlarnaSignInModuleImpl implements KlarnaEventHandler {

private static final String PARAM_NAME_ACTION = "action";
private static final String PARAM_NAME_KLARNA_MESSAGE_EVENT = "klarnaMessageEvent";
private static final String PARAM_NAME_PARAMS = "params";

public static final String NAME = "RNKlarnaSignIn";

public static final String SIGN_IN_SDK_NOT_INITIALIZED = "SIGN_IN_SDK_NOT_INITIALIZED";

private final ReactApplicationContext reactAppContext;
private final HashMap<String, KlarnaSignInData> signInSDKMap;

public KlarnaSignInModuleImpl(ReactApplicationContext reactAppContext) {
this.reactAppContext = reactAppContext;
this.signInSDKMap = new HashMap<String, KlarnaSignInData>();
}

/* Module private methods */

private KlarnaEnvironment environmentFrom(@NonNull String value) {
return switch (value) {
case "playground" -> KlarnaEnvironment.PLAYGROUND;
case "staging" -> KlarnaEnvironment.STAGING;
default -> KlarnaEnvironment.PRODUCTION;
};

}

private KlarnaRegion regionFrom(@NonNull String value) {
return switch (value) {
case "na" -> KlarnaRegion.NA;
case "oc" -> KlarnaRegion.OC;
default -> KlarnaRegion.EU;
};

}

private KlarnaSignInData getInstanceData(KlarnaComponent component) {
for (Map.Entry<String, KlarnaSignInData> entry : signInSDKMap.entrySet()) {
KlarnaSignInData data = entry.getValue();
if (data.sdkInstance == component) {
return data;
}
}
return null;
}

private String getParamsFrom(KlarnaProductEvent event) {
String paramsJson = "{}";
try {
paramsJson = gson.toJson(event.getParams());
} catch (Exception ignored) {
}
return paramsJson;
}

/* Module public methods */
public void init(String instanceId, String environment, String region, String returnUrl, Promise promise) {
KlarnaEnvironment env = environmentFrom(environment);
KlarnaRegion reg = regionFrom(region);
reactAppContext.runOnUiQueueThread(() -> {
KlarnaSignInSDK signInInstance = new KlarnaSignInSDK(reactAppContext.getCurrentActivity(), returnUrl, this, env, reg);
KlarnaSignInData signInData = new KlarnaSignInData(instanceId, signInInstance);
signInSDKMap.put(instanceId, signInData);
promise.resolve(null);
});
}

public void signIn(String instanceId, String clientId, String scope, String market, String locale, String tokenizationId, Promise promise) {
KlarnaSignInData signInData = signInSDKMap.get(instanceId);
if (signInData != null) {
signInData.promise = promise;
signInData.sdkInstance.signIn(clientId, scope, market, locale, tokenizationId);
} else {
promise.reject(SIGN_IN_SDK_NOT_INITIALIZED, "Sign in SDK not initialized");
}
}

/* KlarnaEventHandler methods */

@Override
public void onError(@NonNull KlarnaComponent klarnaComponent, @NonNull KlarnaMobileSDKError klarnaMobileSDKError) {
// Ignore not fatal errors
if (!klarnaMobileSDKError.isFatal()) {
return;
}
KlarnaSignInData data = getInstanceData(klarnaComponent);
if (data != null) {
if (data.promise != null) {
WritableMap map = ArgumentsUtil.createMap(klarnaMobileSDKError.getParams());
data.promise.reject(klarnaMobileSDKError.getName(), klarnaMobileSDKError.getMessage(), map);
signInSDKMap.remove(data.instanceId);
}
}
}

@Override
public void onEvent(@NonNull KlarnaComponent klarnaComponent, @NonNull KlarnaProductEvent klarnaProductEvent) {
KlarnaSignInData data = getInstanceData(klarnaComponent);
if (data != null) {
switch (klarnaProductEvent.getAction()) {
case KlarnaSignInEvent.USER_CANCELLED:
if (data.promise != null) {
ReadableMap eventMap = ArgumentsUtil.createMap(new HashMap<String, Object>() {{
put(PARAM_NAME_ACTION, klarnaProductEvent.getAction());
put(PARAM_NAME_PARAMS, getParamsFrom(klarnaProductEvent));
}});
WritableMap errorMap = ArgumentsUtil.createMap(new HashMap<String, Object>() {{
put(PARAM_NAME_KLARNA_MESSAGE_EVENT, eventMap);
}});
errorMap.putString("sessionId", klarnaProductEvent.getSessionId());
data.promise.reject(klarnaProductEvent.getAction(), errorMap);
signInSDKMap.remove(data.instanceId);
}
break;
case KlarnaSignInEvent.SIGN_IN_TOKEN:
if (data.promise != null) {
ReadableMap event = ArgumentsUtil.createMap(new HashMap<String, Object>() {{
put(PARAM_NAME_ACTION, klarnaProductEvent.getAction());
put(PARAM_NAME_PARAMS, getParamsFrom(klarnaProductEvent));
}});
data.promise.resolve(event);
signInSDKMap.remove(data.instanceId);
}
break;
default:
break;
}
}
}
}
Loading