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

Turbo Module for Adding to Wallet #33028

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
Draft
29 changes: 29 additions & 0 deletions RTNWallet/android/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
buildscript {
ext.safeExtGet = {prop, fallback ->
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}
repositories {
google()
gradlePluginPortal()
}
dependencies {
classpath("com.android.tools.build:gradle:7.3.1")
}
}

apply plugin: 'com.android.library'
apply plugin: 'com.facebook.react'

android {
compileSdkVersion safeExtGet('compileSdkVersion', 33)
namespace "com.rtnwallet"
}

repositories {
mavenCentral()
google()
}

dependencies {
implementation 'com.facebook.react:react-native'
}
148 changes: 148 additions & 0 deletions RTNWallet/android/src/main/java/com/rtnwallet/WalletModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package com.rtnwallet

import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.turbomodule.core.interfaces.TurboModule
import com.google.android.gms.common.api.ApiException
import com.google.android.gms.tapandpay.TapAndPay
import com.google.android.gms.tapandpay.TapAndPayClient
import com.google.android.gms.tapandpay.TapAndPayStatusCodes
import com.google.android.gms.tapandpay.issuer.PushTokenizeRequest
import com.google.android.gms.tapandpay.issuer.UserAddress
import kotlinx.coroutines.*


@ReactModule(name = WalletModule.NAME)
class WalletModule(reactContext: ReactApplicationContext) : NativeWalletSpec(reactContext), TurboModule {
private class WalletCreationItem(val model: Map<String, Any>) {
object Keys {
const val tokenServiceProvider = "tokenServiceProvider"
const val network = "network"
const val opaquePaymentCard = "opaquePaymentCard"
const val displayName = "displayName"
const val lastDigits = "lastDigits"
const val name = "name"
const val phone = "phone"
const val userAddress = "userAddress"
const val address1 = "address1"
const val address2 = "address2"
const val city = "city"
const val state = "state"
const val country = "country"
const val postal_code = "postal_code"
}

private val addressData: Map<String, Any>
get() = model[Keys.userAddress] as? Map<String, Any> ?: HashMap()

val tokenServiceProvider: Int
get() = when ((model[Keys.tokenServiceProvider] as? String ?: "").toUpperCase()) {
"TOKEN_PROVIDER_AMEX" -> TapAndPay.TOKEN_PROVIDER_AMEX
"TOKEN_PROVIDER_MASTERCARD" -> TapAndPay.TOKEN_PROVIDER_MASTERCARD
"TOKEN_PROVIDER_VISA" -> TapAndPay.TOKEN_PROVIDER_VISA
"TOKEN_PROVIDER_DISCOVER" -> TapAndPay.TOKEN_PROVIDER_DISCOVER
else -> 1000
}

val network: Int
get() = when ((model[Keys.network] as? String ?: "").toUpperCase()) {
"AMEX" -> TapAndPay.CARD_NETWORK_AMEX
"DISCOVER" -> TapAndPay.CARD_NETWORK_DISCOVER
"MASTERCARD" -> TapAndPay.CARD_NETWORK_MASTERCARD
"VISA" -> TapAndPay.CARD_NETWORK_VISA
else -> 1000
}

val opcBytes: ByteArray
get() = (model[Keys.opaquePaymentCard] as? String ?: "").toByteArray()

val displayName: String
get() = model[Keys.displayName] as? String ?: ""

val lastDigits: String
get() = model[Keys.lastDigits] as? String ?: ""

val userAddress: UserAddress
get() = UserAddress.newBuilder()
.setName(addressData[Keys.name] as? String ?: "")
.setAddress1(addressData[Keys.address1] as? String ?: "")
.setAddress2(addressData[Keys.address2] as? String ?: "")
.setLocality(addressData[Keys.city] as? String ?: "")
.setAdministrativeArea(addressData[Keys.state] as? String ?: "")
.setCountryCode(addressData[Keys.country] as? String ?: "")
.setPostalCode(addressData[Keys.postal_code] as? String ?: "")
.setPhoneNumber(addressData[Keys.phone] as? String ?: "")
.build()
}

private val tapAndPayClient: TapAndPayClient by lazy {
TapAndPay.getClient(reactContext.currentActivity ?: throw Exception("Current Activity not found"))
}

private val coroutineScope = CoroutineScope(Dispatchers.Main + Job())

override fun getDeviceDetails(promise: Promise) {
coroutineScope.launch {
try {
val walletID = async(Dispatchers.IO) { getWalletID() }
val hardwareID = async(Dispatchers.IO) { getHardwareID() }
val result = WritableNativeMap().apply {
putString("walletID", walletID.await())
putString("hardwareID", hardwareID.await())
}
promise.resolve(result)
} catch (e: Exception) {
promise.reject("DEVICE_DETAILS_ERROR", "Failed to retrieve device details", e)
}
}
}


override fun handleWalletCreationResponse(data: ReadableMap, promise: Promise) {
try {
val context = reactContext.currentActivity ?: throw Exception("Current Activity not found")
val walletCreationItem = WalletCreationItem(data)
val pushTokenizeRequest = PushTokenizeRequest.Builder()
.setOpaquePaymentCard(walletCreationItem.opcBytes)
.setNetwork(walletCreationItem.network)
.setTokenServiceProvider(walletCreationItem.tokenServiceProvider)
.setDisplayName(walletCreationItem.displayName)
.setLastDigits(walletCreationItem.lastDigits)
.setUserAddress(walletCreationItem.userAddress)
.build()
tapAndPayClient.pushTokenize(context, pushTokenizeRequest, requestPushTokenize)

// Assuming success handling here. Adjust according to actual process
promise.resolve(null)
} catch (e: Exception) {
promise.reject("WALLET_CREATION_ERROR", "Failed to add card to Google Pay", e)
}
}

private fun getWalletID(completion: (String?, Exception?) -> Unit) {
tapAndPayClient.activewalletID.addOnCompleteListener { task ->
if (task.isSuccessful && task.result != null) {
completion(task.result, null)
} else {
completion(null, task.exception)
}
}
}

private fun getHardwareID(completion: (String?, Exception?) -> Unit) {
tapAndPayClient.stablehardwareID.addOnCompleteListener { task ->
if (task.isSuccessful && task.result != null) {
completion(task.result, null)
} else {
completion(null, task.exception)
}
}
}


companion object {
const val NAME = "RTNWallet"
}
}
30 changes: 30 additions & 0 deletions RTNWallet/android/src/main/java/com/rtnwallet/WalletPackage.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.rtnwallet

import com.facebook.react.TurboReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProvider

class WalletPackage : TurboReactPackage() {
override fun getModule(name: String?, reactContext: ReactApplicationContext): NativeModule? =
if (name == WalletModule.NAME) {
WalletModule(reactContext)
} else {
null
}

override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider {
mapOf(
WalletModule.NAME to ReactModuleInfo(
WalletModule.NAME,
WalletModule.NAME,
false, // canOverrideExistingModule
false, // needsEagerInit
true, // hasConstants
false, // isCxxModule
true // isTurboModule
)
)
}
}
9 changes: 9 additions & 0 deletions RTNWallet/ios/RTNWallet.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#import <RTNCWalletSpec/RTNWalletSpec.h>

NS_ASSUME_NONNULL_BEGIN

@interface RTNWallet : NSObject <NativeWalletSpec>

@end

NS_ASSUME_NONNULL_END
34 changes: 34 additions & 0 deletions RTNWallet/ios/RTNWallet.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#import "RTNWalletSpec.h"
#import "RTNWallet.h"

@implementation RTNWallet

RCT_EXPORT_MODULE();

RCT_EXPORT_METHOD(handleWalletCreationResponse:(NSDictionary *)data
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {

NSString *encryptedPassData = data[@"encryptedPassData"];
NSString *activationData = data[@"activationData"];
NSString *ephemeralPublicKey = data[@"ephemeralPublicKey"];

// Convert strings to NSData
NSData *encryptedPassDataBytes = [[NSData alloc] initWithBase64EncodedString:encryptedPassData options:0];
NSData *activationDataBytes = [[NSData alloc] initWithBase64EncodedString:activationData options:0];
NSData *ephemeralPublicKeyBytes = [[NSData alloc] initWithBase64EncodedString:ephemeralPublicKey options:0];

if (!encryptedPassDataBytes || !activationDataBytes || !ephemeralPublicKeyBytes) {
reject(@"ERROR", @"Invalid data provided", nil);
return;
}


}

RCT_EXPORT_METHOD(getDeviceDetails:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {

}

@end
19 changes: 19 additions & 0 deletions RTNWallet/ios/rtn-wallet.podspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
require "json"

package = JSON.parse(File.read(File.join(__dir__, "package.json")))

Pod::Spec.new do |s|
s.name = "rtn-wallet"
s.version = package["version"]
s.summary = package["description"]
s.description = package["description"]
s.homepage = package["homepage"]
s.license = package["license"]
s.platforms = { :ios => "11.0" }
s.author = package["author"]
s.source = { :git => package["repository"], :tag => "#{s.version}" }

s.source_files = "ios/**/*.{h,m,mm,swift}"

install_modules_dependencies(s)
end
29 changes: 29 additions & 0 deletions RTNWallet/js/NativeWallet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {TurboModule, TurboModuleRegistry} from 'react-native';

export interface Spec extends TurboModule {
// Method to get device details for wallet creation
getDeviceDetails(): Promise<{
walletAccountID?: string; // Android
deviceID?: string; // Android
// Add any iOS-specific details if needed
}>;

// Method to handle wallet creation response
handleWalletCreationResponse(data: {
// Common return values
cardToken: string;
// Android-specific return values
opaquePaymentCard?: string;
userAddress?: string;
network?: string;
tokenServiceProvider?: string;
displayName?: string;
lastDigits?: string;
// iOS-specific return values
encryptedPassData?: string;
activationData?: string;
ephemeralPublicKey?: string;
}): Promise<boolean>;
}

export default TurboModuleRegistry.get<Spec>('RTNWallet');
71 changes: 71 additions & 0 deletions RTNWallet/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
{
"name": "react-native-wallet",
"version": "0.0.1",
"description": "A cross platform native module for React-Native to add payment cards to Apple Pay and Google Pay",
"main": "index.js",
"scripts": {
"build": "echo \"Warning: no build process has been specified yet\"",
"lint": "eslint index.js",
"prettier": "prettier --write .",
"prettier-watch": "onchange \"**/*.js\" -- prettier --write --ignore-unknown {{changed}}",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Expensify/react-native-wallet.git"
},
"files": [
"js",
"android",
"ios",
"rtn-wallet.podspec",
"!android/build",
"!ios/build",
"!**/__tests__",
"!**/__fixtures__",
"!**/__mocks__"
],
"keywords": [
"React",
"React-Native",
"Passkit",
"Wallet"
],
"author": "Expensify, Inc.",
"license": "MIT",
"bugs": {
"url": "https://github.com/Expensify/react-native-wallet/issues"
},
"homepage": "https://github.com/Expensify/react-native-wallet#readme",
"devDependencies": {
"@babel/core": "7.20.12",
"@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.2.5",
"eslint": "^7.6.0",
"eslint-config-expensify": "^2.0.24",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-jsx-a11y": "^6.6.1",
"eslint-plugin-react": "^7.31.10",
"metro-react-native-babel-preset": "^0.72.3",
"prettier": "^2.8.8"
},
"peerDependencies": {
"react": "*",
"react-native": "*"
},
"engines": {
"node": "20.9.0",
"npm": "10.1.0"
},
"codegenConfig": {
"name": "RTNWalletSpec",
"type": "modules",
"jsSrcsDir": "js",
"android": {
"javaPackageName": "com.rtnwallet"
}
}
}

19 changes: 19 additions & 0 deletions RTNWallet/rtn-wallet.podspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
require "json"

package = JSON.parse(File.read(File.join(__dir__, "package.json")))

Pod::Spec.new do |s|
s.name = "rtn-wallet"
s.version = package["version"]
s.summary = package["description"]
s.description = package["description"]
s.homepage = package["homepage"]
s.license = package["license"]
s.platforms = { :ios => "11.0" }
s.author = package["author"]
s.source = { :git => package["repository"], :tag => "#{s.version}" }

s.source_files = "ios/**/*.{h,m,mm,swift}"

install_modules_dependencies(s)
end
Loading
Loading