Skip to content

Commit

Permalink
Update Wallet native module to have an unified interface (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
zoontek authored Nov 14, 2023
1 parent 8f3905f commit 99b6f3e
Show file tree
Hide file tree
Showing 14 changed files with 451 additions and 513 deletions.
5 changes: 2 additions & 3 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ apply plugin: "com.android.application"
apply plugin: "com.facebook.react"

apply from: "./dotenv.gradle"
apply from: "../../node_modules/@sentry/react-native/sentry.gradle"

/**
* This is the configuration block to customize your React Native Android app.
Expand Down Expand Up @@ -81,8 +80,8 @@ android {
applicationId "io.swan.id"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1694438152
versionName "1.2.0"
versionCode 1699970354
versionName "1.2.1"
}
signingConfigs {
debug {
Expand Down
175 changes: 120 additions & 55 deletions android/app/src/main/java/io/swan/rnwallet/RNWalletModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Intent;
import android.util.Base64;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
Expand All @@ -25,9 +26,10 @@
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;

import java.util.HashMap;
import org.json.JSONException;
import org.json.JSONObject;

import java.util.List;
import java.util.Map;

@ReactModule(name = RNWalletModule.NAME)
public class RNWalletModule extends ReactContextBaseJavaModule implements ActivityEventListener {
Expand Down Expand Up @@ -55,6 +57,42 @@ public String getName() {
@Override
public void onNewIntent(Intent intent) {}

private void keepPromisePending(final Promise promise) {
if (mPromise != null) {
mPromise.reject("wallet_error", "Promise aborted by incoming new operation");
}
mPromise = promise;
}

private void resolvePendingPromise(final Object data) {
if (mPromise != null) {
mPromise.resolve(data);
mPromise = null;
}
}

private void rejectPendingPromise(final String message) {
if (mPromise != null) {
mPromise.reject("wallet_error", message);
mPromise = null;
}
}

private Object base64ToJsonHex(@Nullable String base64) {
if (base64 == null) {
return JSONObject.NULL;
}

byte[] bytes = Base64.decode(base64, Base64.DEFAULT);
StringBuilder hex = new StringBuilder();

for (byte aByte : bytes) {
hex.append(String.format("%02x", aByte));
}

return hex.toString();
}

@ReactMethod
public void getCards(final Promise promise) {
tapAndPayClient
Expand All @@ -69,8 +107,8 @@ public void onComplete(@NonNull Task<List<TokenInfo>> task) {
for (TokenInfo token : task.getResult()) {
final WritableMap card = Arguments.createMap();

card.putString("FPANSuffix", token.getFpanLastFour());
card.putString("identifier", token.getIssuerTokenId());
card.putString("lastFourDigits", token.getFpanLastFour());
card.putString("passURLOrToken", token.getIssuerTokenId());
card.putBoolean("canBeAdded", false); // card already in wallet

cards.pushMap(card);
Expand All @@ -80,91 +118,118 @@ public void onComplete(@NonNull Task<List<TokenInfo>> task) {
} else {
@Nullable Exception exception = task.getException();

promise.reject("GET_CARDS_ERROR",
promise.reject("wallet_error",
exception != null ? exception.getMessage() : "Unknown error");
}
}
});
}

@ReactMethod
public void openCard(final String token, final Promise promise) {
ViewTokenRequest request = new ViewTokenRequest.Builder()
.setIssuerTokenId(token)
.setTokenServiceProvider(TapAndPay.TOKEN_PROVIDER_MASTERCARD)
.build();

tapAndPayClient
.viewToken(request)
.addOnCompleteListener(new OnCompleteListener<PendingIntent>() {

@Override
public void onComplete(@NonNull Task<PendingIntent> task) {
if (task.isSuccessful()) {
try {
task.getResult().send();
promise.resolve(null);
} catch (PendingIntent.CanceledException exception) {
promise.reject("OPEN_CARD_IN_WALLET_ERROR", exception.getMessage());
}
} else {
@Nullable Exception exception = task.getException();

promise.reject("OPEN_CARD_IN_WALLET_ERROR",
exception != null ? exception.getMessage() : "Unknown error");
}
}
});
public void getSignatureData(final ReadableMap data, final Promise promise) {
promise.resolve(null); // Not needed on Android, only for API parity
}

@ReactMethod
public void addCard(final ReadableMap data, final Promise promise) {
@Nullable String cardHolderName = data.getString("cardHolderName");
@Nullable String cardSuffix = data.getString("cardSuffix");
@Nullable String opc = data.getString("opc");
@Nullable String holderName = data.getString("holderName");
@Nullable String lastFourDigits = data.getString("lastFourDigits");

if (cardHolderName == null || cardSuffix == null || opc == null) {
promise.reject("ADD_CARD_ERROR", "Input is not correctly formatted");
if (holderName == null || lastFourDigits == null) {
promise.reject("wallet_error", "Input is not correctly formatted");
return;
}

@Nullable Activity activity = getCurrentActivity();

if (activity == null) {
promise.reject("ADD_CARD_ERROR", "Could not get current activity");
promise.reject("wallet_error", "Could not get current activity");
return;
}

@Nullable String activationData = data.getString("activationData");
@Nullable String encryptedData = data.getString("encryptedData");
@Nullable String ephemeralPublicKey = data.getString("ephemeralPublicKey");
@Nullable String iv = data.getString("iv");
@Nullable String oaepHashingAlgorithm = data.getString("oaepHashingAlgorithm");
@Nullable String publicKeyFingerprint = data.getString("publicKeyFingerprint");

JSONObject opcJson = new JSONObject();

try {
JSONObject info = new JSONObject();

info.put("encryptedData", base64ToJsonHex(encryptedData));
info.put("iv", base64ToJsonHex(iv));
info.put("publicKeyFingerprint", base64ToJsonHex(publicKeyFingerprint));
info.put("encryptedKey", base64ToJsonHex(ephemeralPublicKey));
info.put("oaepHashingAlgorithm",
oaepHashingAlgorithm != null && oaepHashingAlgorithm.contains("SHA256") ? "SHA256" : "SHA512");

opcJson.put("cardInfo", info);

if (activationData != null) {
opcJson.put("tokenizationAuthenticationValue", activationData);
}
} catch (JSONException exception) {
promise.reject("wallet_error", exception.getMessage());
return;
}

byte[] opc = Base64.encode(opcJson.toString().getBytes(), Base64.DEFAULT);

PushTokenizeRequest request = new PushTokenizeRequest.Builder()
.setOpaquePaymentCard(opc.getBytes())
.setOpaquePaymentCard(opc)
.setNetwork(TapAndPay.CARD_NETWORK_MASTERCARD)
.setTokenServiceProvider(TapAndPay.TOKEN_PROVIDER_MASTERCARD)
.setDisplayName("Swan card")
.setLastDigits(cardSuffix)
.setLastDigits(lastFourDigits)
.build();

mPromise = promise;
keepPromisePending(promise);
tapAndPayClient.pushTokenize(activity, request, REQUEST_CODE_PUSH_TOKENIZE);
}

@Override
public void onActivityResult(Activity activity, int requestCode, int resultCode, @Nullable Intent intent) {
if (requestCode != REQUEST_CODE_PUSH_TOKENIZE || mPromise == null) {
return;
if (requestCode == REQUEST_CODE_PUSH_TOKENIZE) {
boolean success = resultCode == Activity.RESULT_OK;

if (success || resultCode == Activity.RESULT_CANCELED) {
resolvePendingPromise(success);
} else {
rejectPendingPromise("Could not provision card");
}
}
}

switch (resultCode) {
case Activity.RESULT_OK:
mPromise.resolve(true);
break;
case Activity.RESULT_CANCELED:
mPromise.resolve(false);
break;
default:
mPromise.reject("ADD_CARD_ERROR", "Could not provision card");
}
@ReactMethod
public void showCard(final String token, final Promise promise) {
ViewTokenRequest request = new ViewTokenRequest.Builder()
.setIssuerTokenId(token)
.setTokenServiceProvider(TapAndPay.TOKEN_PROVIDER_MASTERCARD)
.build();

// Remove promise so it cannot be reused
mPromise = null;
tapAndPayClient
.viewToken(request)
.addOnCompleteListener(new OnCompleteListener<PendingIntent>() {

@Override
public void onComplete(@NonNull Task<PendingIntent> task) {
if (task.isSuccessful()) {
try {
task.getResult().send();
promise.resolve(null);
} catch (PendingIntent.CanceledException exception) {
promise.reject("wallet_error", exception.getMessage());
}
} else {
@Nullable Exception exception = task.getException();

promise.reject("wallet_error",
exception != null ? exception.getMessage() : "Unknown error");
}
}
});
}
}
Loading

0 comments on commit 99b6f3e

Please sign in to comment.