diff --git a/android b/android index 8be2eecf..6a7cf119 160000 --- a/android +++ b/android @@ -1 +1 @@ -Subproject commit 8be2eecf83bad137915837cb434e32a50a6fb7a3 +Subproject commit 6a7cf119b1debcd6c79977d3d334e2addcecf3cb diff --git a/ios b/ios index e6e36b7c..25cb4e53 160000 --- a/ios +++ b/ios @@ -1 +1 @@ -Subproject commit e6e36b7c720e0ce69a1cfce57cb4fe1146e38b99 +Subproject commit 25cb4e534106382a496456550103228fd50ce38b diff --git a/package.json b/package.json index d080ee52..18a55f21 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hyperswitch", - "version": "1.0.0", + "version": "1.0.1", "private": true, "scripts": { "start": "react-native start --reset-cache", @@ -9,7 +9,7 @@ "re:format": "rescript format -all", "web": "webpack serve --mode=development --config reactNativeWeb/webpack.config.js", "ios": "cd ios && rm -rf build && pod install && cd .. && react-native run-ios", - "android": "react-native run-android && yarn run adb", + "android": "react-native run-android --appIdSuffix demoapp --main-activity .demoapp.MainActivity && yarn run adb", "web:next": "next", "bundle:android": "react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/hyperswitch.bundle", "bundle:ios": "react-native bundle --platform ios --dev false --entry-file index.js --bundle-output ios/hyperswitchSDK/Core/Resources/hyperswitch.bundle", @@ -26,6 +26,7 @@ "clean:pod": "cd ios && pod deintegrate && cd ..", "clean:gradle:cache": "rm -rf ~/.gradle", "clean:pod:cache": "pod cache clean --all", + "deploy-to-s3": "node ./scripts/pushToS3.js", "lint": "eslint .", "test": "jest", "prepare": "husky" diff --git a/patches/@react-native+assets-registry+0.75.4.patch b/patches/@react-native+assets-registry+0.75.4.patch new file mode 100644 index 00000000..fc931b4a --- /dev/null +++ b/patches/@react-native+assets-registry+0.75.4.patch @@ -0,0 +1,36 @@ +diff --git a/node_modules/@react-native/assets-registry/registry.js b/node_modules/@react-native/assets-registry/registry.js +index 02470da..3851d92 100644 +--- a/node_modules/@react-native/assets-registry/registry.js ++++ b/node_modules/@react-native/assets-registry/registry.js +@@ -10,28 +10,15 @@ + + 'use strict'; + +-export type PackagerAsset = { +- +__packager_asset: boolean, +- +fileSystemLocation: string, +- +httpServerLocation: string, +- +width: ?number, +- +height: ?number, +- +scales: Array, +- +hash: string, +- +name: string, +- +type: string, +- ... +-}; ++const assets = []; + +-const assets: Array = []; +- +-function registerAsset(asset: PackagerAsset): number { ++function registerAsset(asset) { + // `push` returns new array length, so the first asset will + // get id 1 (not 0) to make the value truthy + return assets.push(asset); + } + +-function getAssetByID(assetId: number): PackagerAsset { ++function getAssetByID(assetId) { + return assets[assetId - 1]; + } + diff --git a/react-native.config.js b/react-native.config.js index cbe85c8f..75427edf 100644 --- a/react-native.config.js +++ b/react-native.config.js @@ -1,3 +1,8 @@ module.exports = { - assets:['./assets/fonts/'], -} \ No newline at end of file + assets: ['./assets/fonts/'], + project: { + android: { + appName: 'demo-app', + }, + }, +}; diff --git a/reactNativeWeb/version.json b/reactNativeWeb/version.json new file mode 100644 index 00000000..857d9d28 --- /dev/null +++ b/reactNativeWeb/version.json @@ -0,0 +1,3 @@ +{ + "version": "1.0.7" +} \ No newline at end of file diff --git a/reactNativeWeb/webpack.config.js b/reactNativeWeb/webpack.config.js index 8d439dc4..345cdf31 100644 --- a/reactNativeWeb/webpack.config.js +++ b/reactNativeWeb/webpack.config.js @@ -6,8 +6,13 @@ const HtmlWebpackPlugin = require('html-webpack-plugin'); const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); const appDirectory = path.resolve(__dirname); const {presets, plugins} = require(`${appDirectory}/babel.config.js`); -const isDevelopment = process.env.NODE_ENV !== 'production'; -console.log('dev mode --- >', isDevelopment); +// const isDevelopment = process.env.NODE_ENV !== 'production'; +const isDevelopment = false; +const repoVersion = require("./version.json").version; +const majorVersion = "v" + repoVersion.split(".")[0]; +const repoPublicPath = `/mobile/${repoVersion}/mobile/${majorVersion}`; + +// console.log('dev mode --- >', isDevelopment); const compileNodeModules = [ // Add every react-native package that needs compiling // 'react-native-gesture-handler', @@ -79,8 +84,8 @@ module.exports = { }, output: { path: path.resolve(appDirectory, 'dist'), - publicPath: '/', filename: 'index.bundle.js', + publicPath: `${repoPublicPath}/`, }, devtool: 'source-map', devServer: { diff --git a/scripts/prepareS3.js b/scripts/prepareS3.js new file mode 100644 index 00000000..1c6c80f4 --- /dev/null +++ b/scripts/prepareS3.js @@ -0,0 +1,209 @@ +const { PutObjectCommand } = require("@aws-sdk/client-s3"); +const { + CreateInvalidationCommand, + GetDistributionConfigCommand, + UpdateDistributionCommand, + GetInvalidationCommand, + CloudFrontClient, +} = require("@aws-sdk/client-cloudfront"); +const FileSystem = require("fs"); +const { globSync } = require("fast-glob"); +const Mime = require("mime-types"); +const { S3Client } = require("@aws-sdk/client-s3"); + +const CACHE_CONTROL = "max-age=315360000"; +const EXPIRATION_DATE = "Thu, 31 Dec 2037 23:55:55 GMT"; + +function withSlash(str) { + return typeof str === "string" ? `/${str}` : ""; +} + +function withHyphen(str) { + return typeof str === "string" ? `${str}-` : ""; +} + +function createCloudFrontClient(region) { + return new CloudFrontClient({ region }); +} + +function createS3Client(region) { + return new S3Client({ region }); +} + +async function doInvalidation(distributionId, urlPrefix, region, s3Bucket) { + const cloudfrontClient = createCloudFrontClient(region); + const cloudfrontInvalidationRef = new Date().toISOString(); + + const cfParams = { + DistributionId: distributionId, + InvalidationBatch: { + CallerReference: cloudfrontInvalidationRef, + Paths: { + Quantity: s3Bucket === process.env.S3_SANDBOX_BUCKET ? 2 : 1, + Items: + s3Bucket === process.env.S3_SANDBOX_BUCKET + ? [`/mobile${withSlash(urlPrefix)}/*`, `/mobile${withSlash("v0")}/*`] + : [`/mobile${withSlash(urlPrefix)}/*`], + }, + }, + }; + + const command = new CreateInvalidationCommand(cfParams); + let response = await cloudfrontClient.send(command); + const invalidationId = response.Invalidation.Id; + let retryCounter = 0; + + while (response.Invalidation.Status === "InProgress" && retryCounter < 100) { + await new Promise((resolve) => setTimeout(resolve, 3000)); + const getInvalidationParams = { + DistributionId: distributionId, + Id: invalidationId, + }; + const statusCommand = new GetInvalidationCommand(getInvalidationParams); + response = await cloudfrontClient.send(statusCommand); + retryCounter++; + } + + if (retryCounter >= 100) { + console.log(`Still InProgress after ${retryCounter} retries`); + } +} + +async function getDistribution(distributionId, region) { + const getDistributionConfigCmd = new GetDistributionConfigCommand({ + Id: distributionId, + }); + + const cloudfrontClient = createCloudFrontClient(region); + const { DistributionConfig, ETag } = await cloudfrontClient.send( + getDistributionConfigCmd + ); + + return { DistributionConfig, ETag }; +} + +async function updateDistribution({ + urlPrefix, + distributionId, + version, + DistributionConfig, + ETag, + region, +}) { + const cloudfrontClient = createCloudFrontClient(region); + let matchingItem; + matchingItem = DistributionConfig.Origins.Items.find((item) => + item.Id.startsWith("mobile-v1") + ); + if (matchingItem) { + matchingItem.OriginPath = `/${version}`; + } else { + const defaultItem = + DistributionConfig.Origins.Items.find((item) => item.OriginPath === "") || + DistributionConfig.Origins.Items[0]; + + if (defaultItem) { + const clonedOrigin = JSON.parse(JSON.stringify(defaultItem)); + clonedOrigin.Id = `${withHyphen(urlPrefix)}${clonedOrigin.Id}`; + clonedOrigin.OriginPath = `/${version}`; + + DistributionConfig.Origins.Items.unshift(clonedOrigin); + DistributionConfig.Origins.Quantity += 1; + + if (DistributionConfig.CacheBehaviors.Items.length > 0) { + const clonedBehavior = JSON.parse( + JSON.stringify(DistributionConfig.CacheBehaviors.Items[0]) + ); + if (urlPrefix) clonedBehavior.PathPattern = `${urlPrefix}/*`; + clonedBehavior.TargetOriginId = clonedOrigin.Id; + + DistributionConfig.CacheBehaviors.Items.unshift(clonedBehavior); + DistributionConfig.CacheBehaviors.Quantity += 1; + } + } + } + + const updateDistributionCmd = new UpdateDistributionCommand({ + DistributionConfig, + Id: distributionId, + IfMatch: ETag, + }); + + await cloudfrontClient.send(updateDistributionCmd); +} + +async function uploadFile(s3Bucket, version, urlPrefix, distFolder, region) { + const entries = globSync(`${distFolder}/**/*`); + const s3Client = createS3Client(region); + + for (const val of entries) { + const fileName = val.replace(`${distFolder}/`, ""); + const bufferData = FileSystem.readFileSync(val); + const mimeType = Mime.lookup(val); + + const params = { + Bucket: s3Bucket, + Key: `${version}/mobile${withSlash(urlPrefix)}/${fileName}`, + Body: bufferData, + Metadata: { + "Cache-Control": CACHE_CONTROL, + Expires: EXPIRATION_DATE, + }, + ContentType: mimeType, + }; + + await s3Client.send(new PutObjectCommand(params)); + + if (s3Bucket === process.env.S3_SANDBOX_BUCKET) { + const sandboxParams = { + ...params, + Key: `${version}/mobile${withSlash("v0")}/${fileName}`, + }; + await s3Client.send(new PutObjectCommand(sandboxParams)); + } + + console.log(`Successfully uploaded to ${params.Key}`); + } +} + +const run = async (params) => { + console.log("run parameters ---", params); + let { s3Bucket, distributionId, urlPrefix, version, distFolder, region } = + params; + try { + const isVersioned = urlPrefix === "v0" || urlPrefix === "v1"; + + if (isVersioned) { + await uploadFile(s3Bucket, version, "v0", distFolder, region); + await uploadFile(s3Bucket, version, "v1", distFolder, region); + } else { + await uploadFile(s3Bucket, version, urlPrefix, distFolder, region); + } + if (s3Bucket !== process.env.S3_PROD_BUCKET) { + const distributionInfo = await getDistribution(distributionId, region); + console.log("distributionInfo completed"); + await updateDistribution({ + ...distributionInfo, + distributionId, + urlPrefix, + version, + region, + }); + console.log("updateDistribution completed"); + if (isVersioned) { + await doInvalidation(distributionId, "v0", region, s3Bucket); + await doInvalidation(distributionId, "v1", region, s3Bucket); + } else { + await doInvalidation(distributionId, urlPrefix, region, s3Bucket); + } + console.log("doInvalidation completed"); + } + } catch (err) { + console.error("Error", err); + throw err; + } +}; + +module.exports = { + run, +}; \ No newline at end of file diff --git a/scripts/pushToS3.js b/scripts/pushToS3.js new file mode 100644 index 00000000..01333f82 --- /dev/null +++ b/scripts/pushToS3.js @@ -0,0 +1,15 @@ +const { run } = require("./prepareS3.js"); +const { version } = require("../reactNativeWeb/version.json"); +const path = require("path"); +const BASE_PATH = "mobile"; + +let params = { + s3Bucket: process.env.BUCKET_NAME, + distributionId: process.env.DIST_ID, + urlPrefix: `v${version.split(".")[0]}`, + version: `${BASE_PATH}/${version}`, + distFolder: path.resolve(__dirname, "..", "reactNativeWeb/dist"), + region: process.env.AWS_REGION, +}; + +run(params); \ No newline at end of file diff --git a/server.js b/server.js index 38f8465b..0350ebe9 100644 --- a/server.js +++ b/server.js @@ -64,6 +64,36 @@ app.get('/create-ephemeral-key', async (req, res) => { } }); +app.get('/payment_methods', async (req, res) => { + try { + const response = await fetch( + `https://sandbox.hyperswitch.io/payment_methods`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'api-key': process.env.HYPERSWITCH_SECRET_KEY, + }, + body: JSON.stringify({customer_id: 'hyperswitch_sdk_demo_id'}), + }, + ); + const json = await response.json(); + + res.send({ + customerId: json.customer_id, + paymentMethodId: json.payment_method_id, + clientSecret: json.client_secret, + publishableKey: process.env.HYPERSWITCH_PUBLISHABLE_KEY, + }); + } catch (err) { + return res.status(400).send({ + error: { + message: err.message, + }, + }); + } +}); + app.listen(5252, () => console.log(`Node server listening at http://localhost:5252`), ); diff --git a/src/components/common/CustomInput.res b/src/components/common/CustomInput.res index 423265d1..eac071ce 100644 --- a/src/components/common/CustomInput.res +++ b/src/components/common/CustomInput.res @@ -211,16 +211,7 @@ let make = ( placeholderTextColor={placeholderTextColor->Option.getOr(placeholderColor)} value={state} ?onKeyPress - onChangeText={text => { - logger( - ~logType=INFO, - ~value=text, - ~category=USER_EVENT, - ~eventName=INPUT_FIELD_CHANGED, - (), - ) - setState(text) - }} + onChangeText={text => setState(text)} keyboardType autoFocus autoComplete={#off} diff --git a/src/components/common/CustomKeyboardAvoidingView.res b/src/components/common/CustomKeyboardAvoidingView.res deleted file mode 100644 index 5f9394c4..00000000 --- a/src/components/common/CustomKeyboardAvoidingView.res +++ /dev/null @@ -1,27 +0,0 @@ -@react.component -let make = (~children) => { - let (keyboardVisible, setKeyboardVisible) = React.useState(_ => true) - React.useEffect0(() => { - if ReactNative.Platform.os === #android { - let showListener = ReactNative.Keyboard.addListener(#keyboardDidShow, _ => { - setKeyboardVisible(_ => true) - }) - let hideListener = ReactNative.Keyboard.addListener(#keyboardDidHide, _ => { - setKeyboardVisible(_ => false) - }) - - Some( - () => { - showListener->ReactNative.EventSubscription.remove - hideListener->ReactNative.EventSubscription.remove - }, - ) - } else { - None - } - }) - - children - -} diff --git a/src/components/common/CustomKeyboardAvoidingView/CustomKeyboardAvoidingView.res b/src/components/common/CustomKeyboardAvoidingView/CustomKeyboardAvoidingView.res new file mode 100644 index 00000000..817e5b9c --- /dev/null +++ b/src/components/common/CustomKeyboardAvoidingView/CustomKeyboardAvoidingView.res @@ -0,0 +1,17 @@ +module KeyboardAvoidingView = { + type props = { + behavior: ReactNative.KeyboardAvoidingView.behavior, + style?: ReactNative.Style.t, + children?: React.element, + } + + @module("./CustomKeyboardAvoidingViewImpl") + external make: React.component = "make" +} + +@react.component +let make = (~children) => { + + children + +} diff --git a/src/components/common/CustomKeyboardAvoidingView/CustomKeyboardAvoidingViewImpl.android.res b/src/components/common/CustomKeyboardAvoidingView/CustomKeyboardAvoidingViewImpl.android.res new file mode 100644 index 00000000..7dd74481 --- /dev/null +++ b/src/components/common/CustomKeyboardAvoidingView/CustomKeyboardAvoidingViewImpl.android.res @@ -0,0 +1,2 @@ +type props = ReactNative.View.props +let make = ReactNative.View.make diff --git a/src/components/common/CustomKeyboardAvoidingView/CustomKeyboardAvoidingViewImpl.ios.res b/src/components/common/CustomKeyboardAvoidingView/CustomKeyboardAvoidingViewImpl.ios.res new file mode 100644 index 00000000..e9a505d9 --- /dev/null +++ b/src/components/common/CustomKeyboardAvoidingView/CustomKeyboardAvoidingViewImpl.ios.res @@ -0,0 +1,2 @@ +type props = ReactNative.KeyboardAvoidingView.props +let make = ReactNative.KeyboardAvoidingView.make diff --git a/src/components/common/CustomKeyboardAvoidingView/CustomKeyboardAvoidingViewImpl.web.res b/src/components/common/CustomKeyboardAvoidingView/CustomKeyboardAvoidingViewImpl.web.res new file mode 100644 index 00000000..7dd74481 --- /dev/null +++ b/src/components/common/CustomKeyboardAvoidingView/CustomKeyboardAvoidingViewImpl.web.res @@ -0,0 +1,2 @@ +type props = ReactNative.View.props +let make = ReactNative.View.make diff --git a/src/components/common/CustomPicker.res b/src/components/common/CustomPicker.res index 83210d29..4a3a6325 100644 --- a/src/components/common/CustomPicker.res +++ b/src/components/common/CustomPicker.res @@ -125,6 +125,7 @@ let make = ( ReactNative.Ref.value} + keyboardShouldPersistTaps={#handled} data={items->Array.filter(x => x.name ->String.toLowerCase diff --git a/src/components/common/CustomView.res b/src/components/common/CustomView.res index a1f88daf..e4ef1b9a 100644 --- a/src/components/common/CustomView.res +++ b/src/components/common/CustomView.res @@ -69,6 +69,7 @@ module Wrapper = { let {bgColor} = ThemebasedStyle.useThemeBasedStyle() dp, ~padding=20.->dp, ()), bgColor, diff --git a/src/components/common/Pager.res b/src/components/common/Pager.res index 024165bd..b950722c 100644 --- a/src/components/common/Pager.res +++ b/src/components/common/Pager.res @@ -337,9 +337,11 @@ let make = ( } else { None }}> - - {focused || layout.width != 0. ? child : React.null} - + {focused || layout.width != 0. + ? + child + + : React.null} | None => React.null } diff --git a/src/components/common/TopTabScreenWraper.res b/src/components/common/TopTabScreenWraper.res index ceff9616..9a42c1b1 100644 --- a/src/components/common/TopTabScreenWraper.res +++ b/src/components/common/TopTabScreenWraper.res @@ -3,13 +3,11 @@ open Style @react.component let make = (~children, ~setDynamicHeight, ~isScreenFocus) => { - let (viewHeight, setViewHeight) = React.useState(_ => 0.) + let (viewHeight, setViewHeight) = React.useState(_ => 100.) let updateTabHeight = (event: Event.layoutEvent) => { let {height} = event.nativeEvent.layout - if (viewHeight -. height)->Math.abs > 10. { + if height > 100. && (viewHeight -. height)->Math.abs > 10. { setViewHeight(_ => height) - } else if height == 0. { - setViewHeight(_ => 100.) } } diff --git a/src/components/elements/ButtonElement.res b/src/components/elements/ButtonElement.res index 1386f842..ebeb2128 100644 --- a/src/components/elements/ButtonElement.res +++ b/src/components/elements/ButtonElement.res @@ -345,7 +345,7 @@ let make = ( let confirmApplePay = (var: RescriptCore.Dict.t) => { logger( ~logType=DEBUG, - ~value=var->Js.Json.stringifyAny->Option.getOr("Option.getOr"), + ~value=walletType.payment_method_type, ~category=USER_EVENT, ~paymentMethod=walletType.payment_method_type, ~eventName=APPLE_PAY_CALLBACK_FROM_NATIVE, diff --git a/src/components/elements/Klarna.res b/src/components/elements/Klarna.res index 1cc9b1c7..3267a7a3 100644 --- a/src/components/elements/Klarna.res +++ b/src/components/elements/Klarna.res @@ -80,7 +80,9 @@ let make = ( } Style.dp, ~borderRadius=15., ())}> + keyboardShouldPersistTaps=#handled + pointerEvents=#none + style={Style.viewStyle(~height=220.->Style.dp, ~borderRadius=15., ())}> {React.array( Array.map(paymentMethods, paymentMethod => { ReactNative.Ref.value} + keyboardShouldPersistTaps={#handled} data=hocComponentArr style={viewStyle(~flex=1., ~width=100.->pct, ())} showsHorizontalScrollIndicator=false diff --git a/src/headless/HeadlessUtils.res b/src/headless/HeadlessUtils.res index a1155f9e..3ecad0e2 100644 --- a/src/headless/HeadlessUtils.res +++ b/src/headless/HeadlessUtils.res @@ -70,6 +70,7 @@ let logWrapper = ( sessionId: "", version: "repoVersion", codePushVersion: LoggerUtils.getCodePushVersionNoFromRef(), + clientCoreVersion: LoggerUtils.getClientCoreVersionNoFromRef(), component: MOBILE, value: value->Dict.fromArray->JSON.Encode.object->JSON.stringify, internalMetadata: internalMetadata->Dict.fromArray->JSON.Encode.object->JSON.stringify, diff --git a/src/hooks/AllPaymentHooks.res b/src/hooks/AllPaymentHooks.res index 60417f9e..2077ca08 100644 --- a/src/hooks/AllPaymentHooks.res +++ b/src/hooks/AllPaymentHooks.res @@ -19,7 +19,7 @@ let useApiLogWrapper = () => { ~paymentExperience=?, (), ) => { - let (value, internalMetadata) = switch apiLogType { + let (_value, internalMetadata) = switch apiLogType { | Request => ([("url", url->JSON.Encode.string)], []) | Response => ( [("url", url->JSON.Encode.string), ("statusCode", statusCode->JSON.Encode.string)], @@ -44,7 +44,7 @@ let useApiLogWrapper = () => { } logger( ~logType, - ~value=value->Dict.fromArray->JSON.Encode.object->JSON.stringify, + ~value=apiLogType->JSON.stringifyAny->Option.getOr(""), ~internalMetadata=internalMetadata->Dict.fromArray->JSON.Encode.object->JSON.stringify, ~category=API, ~eventName, @@ -859,3 +859,101 @@ let useDeleteSavedPaymentMethod = () => { } } } + +let useSavePaymentMethod = () => { + let baseUrl = GlobalHooks.useGetBaseUrl()() + let apiLogWrapper = LoggerHook.useApiLogWrapper() + let (nativeProp, _) = React.useContext(NativePropContext.nativePropContext) + let (cardData, _) = React.useContext(CardDataContext.cardDataContext) + + let (month, year) = Validation.getExpiryDates(cardData.expireDate) + let payment_method_data = + [ + ( + "card", + [ + ("card_number", cardData.cardNumber->Validation.clearSpaces->JSON.Encode.string), + ("card_exp_month", month->JSON.Encode.string), + ("card_exp_year", year->JSON.Encode.string), + ] + ->Dict.fromArray + ->JSON.Encode.object, + ), + ] + ->Dict.fromArray + ->JSON.Encode.object + + let body: PaymentMethodListType.redirectType = { + payment_method: "card", + client_secret: nativeProp.pmClientSecret->Option.getOr(""), + payment_method_data, + } + + () => { + let paymentMethodId = nativeProp.paymentMethodManagementId->Option.getOr("") + let uri = `${baseUrl}/payment_methods/${paymentMethodId}/save` + apiLogWrapper( + ~logType=INFO, + ~eventName=ADD_PAYMENT_METHOD_CALL_INIT, + ~url=uri, + ~statusCode="", + ~apiLogType=Request, + ~data=JSON.Encode.null, + (), + ) + + CommonHooks.fetchApi( + ~uri, + ~method_=Post, + ~headers=Utils.getHeader(nativeProp.publishableKey, nativeProp.hyperParams.appId), + ~bodyStr=body->JSON.stringifyAny->Option.getOr(""), + (), + ) + ->Promise.then(resp => { + let statusCode = resp->Fetch.Response.status->string_of_int + if statusCode->String.charAt(0) !== "2" { + resp + ->Fetch.Response.json + ->Promise.then(error => { + apiLogWrapper( + ~url=uri, + ~data=error, + ~statusCode, + ~apiLogType=Err, + ~eventName=ADD_PAYMENT_METHOD_CALL, + ~logType=ERROR, + (), + ) + error->Promise.resolve + }) + } else { + resp + ->Fetch.Response.json + ->Promise.then(data => { + apiLogWrapper( + ~url=uri, + ~data, + ~statusCode, + ~apiLogType=Response, + ~eventName=ADD_PAYMENT_METHOD_CALL, + ~logType=INFO, + (), + ) + data->Promise.resolve + }) + } + }) + ->Promise.catch(err => { + apiLogWrapper( + ~logType=ERROR, + ~eventName=ADD_PAYMENT_METHOD_CALL, + ~url=uri, + ~statusCode="504", + ~apiLogType=NoResponse, + ~data=err->toJson, + (), + ) + err->toJson->Promise.resolve + }) + } +} diff --git a/src/hooks/LoggerHook.res b/src/hooks/LoggerHook.res index ca670f84..4f491aa2 100644 --- a/src/hooks/LoggerHook.res +++ b/src/hooks/LoggerHook.res @@ -1,5 +1,5 @@ -open LoggerUtils open LoggerTypes +open LoggerUtils external toPlatform: ReactNative.Platform.os => string = "%identity" let useCalculateLatency = () => { @@ -54,6 +54,7 @@ let inactiveScreenApiCall = ( sessionId: session_id, version: nativeProp.hyperParams.sdkVersion, codePushVersion: getCodePushVersionNoFromRef(), + clientCoreVersion: getClientCoreVersionNoFromRef(), component: MOBILE, value: "Inactive Screen", internalMetadata: "", @@ -109,6 +110,7 @@ let useLoggerHook = () => { let calculateLatency = useCalculateLatency() let getLoggingEndpointHook = GlobalHooks.useGetLoggingUrl() getGetPushVersion() + getClientCoreVersion() ( ~logType, ~value, @@ -136,6 +138,7 @@ let useLoggerHook = () => { sessionId: nativeProp.sessionId, version: nativeProp.hyperParams.sdkVersion, codePushVersion: getCodePushVersionNoFromRef(), + clientCoreVersion: getClientCoreVersionNoFromRef(), component: MOBILE, value, internalMetadata: internalMetadata->Option.getOr(""), diff --git a/src/hooks/PMListModifier.res b/src/hooks/PMListModifier.res index 6c2c215c..72cb7887 100644 --- a/src/hooks/PMListModifier.res +++ b/src/hooks/PMListModifier.res @@ -252,6 +252,7 @@ let useListModifier = () => { switch switch walletVal.payment_method_type_wallet { | GOOGLE_PAY => ReactNative.Platform.os !== #ios && + WebKit.webType !== #iosWebView && sessionObject.wallet_name !== NONE && sessionObject.connector !== "trustpay" ? { @@ -275,7 +276,9 @@ let useListModifier = () => { x.payment_experience_type_decode === REDIRECT_TO_URL ) | APPLE_PAY => - ReactNative.Platform.os !== #android && sessionObject.wallet_name !== NONE + ReactNative.Platform.os !== #android && + WebKit.webType !== #androidWebView && + sessionObject.wallet_name !== NONE ? { if ReactNative.Platform.os === #web { Promise.make((resolve, _) => { diff --git a/src/hooks/WebButtonHook.res b/src/hooks/WebButtonHook.res index b81dfb00..4877b8f6 100644 --- a/src/hooks/WebButtonHook.res +++ b/src/hooks/WebButtonHook.res @@ -13,7 +13,7 @@ let usePayButton = () => { googlePayButtonColor, buttonBorderRadius, } = ThemebasedStyle.useThemeBasedStyle() - let {launchApplePay} = WebKit.useWebKit() + let {launchApplePay, launchGPay} = WebKit.useWebKit() let addApplePay = (~sessionObject: SessionsType.sessions, ~resolve as _) => { let status = Window.useScript( @@ -83,60 +83,15 @@ let usePayButton = () => { ~appEnv=nativeProp.env, ~requiredFields, ) - let paymentRequest = token.paymentDataRequest->anyTypeToJson - - let onGooglePayButtonClick = (paymentClient: Window.client) => { - try { - paymentClient.loadPaymentData(paymentRequest) - ->Promise.then(paymentData => { - let data = [ - ("error", ""->JSON.Encode.string), - ( - "paymentMethodData", - paymentData - ->JSON.stringify - ->JSON.Encode.string, - ), - ]->Dict.fromArray - Promise.resolve(data) - }) - ->Promise.catch((err: exn) => { - let errorMessage = switch err->Exn.asJsExn { - | Some(error) => - let statusCode = switch error - ->anyTypeToJson - ->Utils.getDictFromJson - ->Dict.get("statusCode") { - | Some(json) => json->JSON.Decode.string->Option.getOr("failed") - | None => "failed" - } - error->Exn.message->Option.getOr(statusCode) - | None => "failed" - } - let data = - [ - ( - "error", - ( - errorMessage == "User closed the Payment Request UI." || - errorMessage == "CANCELED" - ? "Cancel" - : errorMessage - )->JSON.Encode.string, - ), - ("paymentMethodData", JSON.Encode.null), - ]->Dict.fromArray - Promise.resolve(data) - }) - ->Promise.then(data => { - Window.postMessage({"googlePayData": data}->JSON.stringifyAny->Option.getOr(""), "*") - Promise.resolve() - }) - ->ignore - } catch { - | exn => AlertHook.alert(exn->JSON.stringifyAny->Option.getOr("")) - } + let onGooglePayButtonClick = () => { + launchGPay( + GooglePayTypeNew.getGpayTokenStringified( + ~obj=sessionObject, + ~appEnv=nativeProp.env, + ~requiredFields, + ), + ) } React.useEffect1(() => { @@ -146,7 +101,7 @@ let usePayButton = () => { let buttonStyle = { let obj = { - "onClick": () => onGooglePayButtonClick(paymentClient), + "onClick": () => onGooglePayButtonClick(), "buttonType": nativeProp.configuration.appearance.googlePay.buttonType ->anyTypeToString ->String.toLowerCase, diff --git a/src/hooks/WebKit.res b/src/hooks/WebKit.res index e5734394..3e9aa2f5 100644 --- a/src/hooks/WebKit.res +++ b/src/hooks/WebKit.res @@ -1,43 +1,86 @@ +type webType = [#iosWebView | #androidWebView | #pureWeb] + +let webType = if Window.webKit->Nullable.toOption->Option.isSome { + #iosWebView +} else if Window.androidInterface->Nullable.toOption->Option.isSome { + #androidWebView +} else { + #pureWeb +} + type useWebKit = { exitPaymentSheet: string => unit, sdkInitialised: string => unit, launchApplePay: string => unit, + launchGPay: string => unit, } let useWebKit = () => { - let messageHandlers = switch Window.webKit { + let messageHandlers = switch Window.webKit->Nullable.toOption { | Some(webKit) => webKit.messageHandlers | None => None } let exitPaymentSheet = str => { - switch messageHandlers { - | Some(messageHandlers) => - switch messageHandlers.exitPaymentSheet { - | Some(exitPaymentSheet) => exitPaymentSheet.postMessage(str) + switch webType { + | #iosWebView => + switch messageHandlers { + | Some(messageHandlers) => + switch messageHandlers.exitPaymentSheet { + | Some(exitPaymentSheet) => exitPaymentSheet.postMessage(str) + | None => () + } | None => () } - | None => () + | #androidWebView => + switch Window.androidInterface->Nullable.toOption { + | Some(interface) => interface.exitPaymentSheet(str) + | None => () + } + | #pureWeb => () } } let sdkInitialised = str => { - switch messageHandlers { - | Some(messageHandlers) => - switch messageHandlers.sdkInitialised { - | Some(sdkInitialised) => sdkInitialised.postMessage(str) + switch webType { + | #iosWebView => + switch messageHandlers { + | Some(messageHandlers) => + switch messageHandlers.sdkInitialised { + | Some(sdkInitialised) => sdkInitialised.postMessage(str) + | None => () + } | None => () } - | None => () + | #androidWebView => + switch Window.androidInterface->Nullable.toOption { + | Some(interface) => interface.sdkInitialised(str) + | None => () + } + | #pureWeb => () } } let launchApplePay = str => { - switch messageHandlers { - | Some(messageHandlers) => - switch messageHandlers.launchApplePay { - | Some(launchApplePay) => launchApplePay.postMessage(str) + switch webType { + | #iosWebView => + switch messageHandlers { + | Some(messageHandlers) => + switch messageHandlers.launchApplePay { + | Some(launchApplePay) => launchApplePay.postMessage(str) + | None => () + } + | None => () + } + | _ => () + } + } + let launchGPay = str => { + switch webType { + | #androidWebView => + switch Window.androidInterface->Nullable.toOption { + | Some(interface) => interface.launchGPay(str) | None => () } - | None => () + | _ => () } } - {exitPaymentSheet, sdkInitialised, launchApplePay} + {exitPaymentSheet, sdkInitialised, launchApplePay, launchGPay} } diff --git a/src/pages/hostedCheckout/HostedCheckout.res b/src/pages/hostedCheckout/HostedCheckout.res index 5066f3ef..91e8abd2 100644 --- a/src/pages/hostedCheckout/HostedCheckout.res +++ b/src/pages/hostedCheckout/HostedCheckout.res @@ -39,6 +39,7 @@ let make = () => { dp, ())}> dp, ())])}> diff --git a/src/pages/payment/SavedPMListWithLoader.res b/src/pages/payment/SavedPMListWithLoader.res index 739b82b8..9ccbd418 100644 --- a/src/pages/payment/SavedPMListWithLoader.res +++ b/src/pages/payment/SavedPMListWithLoader.res @@ -101,7 +101,7 @@ let make = ( allApiData.savedPaymentMethods == Loading ? - : + : {listArr ->Array.mapWithIndex((item, i) => { : React.null} + } diff --git a/src/pages/paymentMethodsManagement/PaymentMethodListItem.res b/src/pages/paymentMethodsManagement/PaymentMethodListItem.res index 6087bc10..a0179559 100644 --- a/src/pages/paymentMethodsManagement/PaymentMethodListItem.res +++ b/src/pages/paymentMethodsManagement/PaymentMethodListItem.res @@ -1,6 +1,49 @@ open ReactNative open Style +module AddPaymentMethodButton = { + @react.component + let make = () => { + let {component} = ThemebasedStyle.useThemeBasedStyle() + let localeObject = GetLocale.useGetLocalObj() + + ( + // TODO: navigate to ADD_PM_SCREEN + )} + style={viewStyle( + ~paddingVertical=16.->dp, + ~paddingHorizontal=24.->dp, + ~borderBottomWidth=0.8, + ~borderBottomColor=component.borderColor, + ~flexDirection=#row, + ~flexWrap=#nowrap, + ~alignItems=#center, + ~justifyContent=#"space-between", + ~flex=1., + (), + )}> + + dp, ~marginStart=5.->dp, ~marginVertical=10.->dp, ())} + /> + + + + + } +} + module PaymentMethodTitle = { @react.component let make = (~pmDetails: SdkTypes.savedDataType) => { @@ -40,7 +83,7 @@ module PaymentMethodTitle = { } @react.component -let make = (~pmDetails: SdkTypes.savedDataType, ~isLastElement=true, ~handleDelete) => { +let make = (~pmDetails: SdkTypes.savedDataType, ~handleDelete) => { let {component} = ThemebasedStyle.useThemeBasedStyle() let localeObject = GetLocale.useGetLocalObj() @@ -54,7 +97,7 @@ let make = (~pmDetails: SdkTypes.savedDataType, ~isLastElement=true, ~handleDele style={ viewStyle( ~padding=16.->dp, - ~borderBottomWidth={isLastElement ? 0.8 : 0.}, + ~borderBottomWidth=0.8, ~borderBottomColor=component.borderColor, ~flexDirection=#row, ~flexWrap=#nowrap, diff --git a/src/pages/paymentMethodsManagement/PaymentMethodsManagement.res b/src/pages/paymentMethodsManagement/PaymentMethodsManagement.res index ffcd732c..a9ad1643 100644 --- a/src/pages/paymentMethodsManagement/PaymentMethodsManagement.res +++ b/src/pages/paymentMethodsManagement/PaymentMethodsManagement.res @@ -91,8 +91,6 @@ let make = () => { style={viewStyle( ~backgroundColor=component.background, ~width=100.->pct, - ~paddingTop=25.->pct, - ~paddingBottom=20.->pct, ~flex=1., ~justifyContent=#center, ~alignItems=#center, @@ -101,34 +99,25 @@ let make = () => { : savedMethods->Array.length > 0 - ? pct, - ~marginTop=8.->dp, - ~paddingBottom=20.->pct, - (), - )}> - + ? pct, ())}> + {savedMethods ->Array.mapWithIndex((item, i) => { Int.toString} - pmDetails={item} - isLastElement={Some(savedMethods)->Option.getOr([])->Array.length - 1 != i} - handleDelete=handleDeletePaymentMethods + key={i->Int.toString} pmDetails={item} handleDelete=handleDeletePaymentMethods /> }) ->React.array} + + : pct, - ~paddingBottom=40.->pct, ~flex=1., ~alignItems=#center, + ~justifyContent=#center, (), )}> diff --git a/src/routes/PMMangementNavigatorRouter.res b/src/routes/PMMangementNavigatorRouter.res index ce1058df..d99d2c25 100644 --- a/src/routes/PMMangementNavigatorRouter.res +++ b/src/routes/PMMangementNavigatorRouter.res @@ -37,9 +37,7 @@ let make = () => { switch nativeProp.ephemeralKey { | Some(ephemeralKey) => ephemeralKey != "" - ? - - + ? : { showErrorOrWarning(ErrorUtils.errorWarning.invalidEphemeralKey, ()) React.null diff --git a/src/routes/ParentPaymentSheet.res b/src/routes/ParentPaymentSheet.res index 5006870c..0150d8e5 100644 --- a/src/routes/ParentPaymentSheet.res +++ b/src/routes/ParentPaymentSheet.res @@ -46,6 +46,6 @@ let make = () => { | (None, _, _) => }} - + } diff --git a/src/types/LoggerTypes.res b/src/types/LoggerTypes.res index 935204ff..7c47c8e3 100644 --- a/src/types/LoggerTypes.res +++ b/src/types/LoggerTypes.res @@ -3,6 +3,11 @@ type logCategory = API | USER_ERROR | USER_EVENT | MERCHANT_EVENT type logComponent = MOBILE type apiLogType = Request | Response | NoResponse | Err type codePushVersionFetched = CP_NOT_STARTED | CP_VERSION_LOADING | CP_VERSION_LOADED(string) +type sdkVersionFetched = + | PACKAGE_JSON_NOT_STARTED + | PACKAGE_JSON_LOADING + | PACKAGE_JSON_REFERENCE_ERROR + | PACKAGE_JSON_LOADED(string) type eventName = | APP_RENDERED | INACTIVE_SCREEN @@ -48,6 +53,8 @@ type eventName = | DELETE_PAYMENT_METHODS_CALL_INIT | DELETE_PAYMENT_METHODS_CALL | DELETE_SAVED_PAYMENT_METHOD + | ADD_PAYMENT_METHOD_CALL_INIT + | ADD_PAYMENT_METHOD_CALL type logFile = { timestamp: string, @@ -56,6 +63,7 @@ type logFile = { category: logCategory, version: string, codePushVersion: string, + clientCoreVersion: string, value: string, internalMetadata: string, sessionId: string, diff --git a/src/types/SdkTypes.res b/src/types/SdkTypes.res index 86dd5ef3..69a4ba65 100644 --- a/src/types/SdkTypes.res +++ b/src/types/SdkTypes.res @@ -309,6 +309,8 @@ type nativeProp = { publishableKey: string, clientSecret: string, ephemeralKey: option, + paymentMethodManagementId: option, + pmClientSecret: option, customBackendUrl: option, customLogUrl: option, sessionId: string, @@ -895,6 +897,8 @@ let nativeJsonToRecord = (jsonFromNative, rootTag) => { publishableKey, clientSecret: getString(dictfromNative, "clientSecret", ""), ephemeralKey: getOptionString(dictfromNative, "ephemeralKey"), + paymentMethodManagementId: getOptionString(dictfromNative, "paymentMethodId"), + pmClientSecret: getOptionString(dictfromNative, "pmClientSecret"), customBackendUrl, customLogUrl, sessionId: "", diff --git a/src/utility/logics/LoggerUtils.res b/src/utility/logics/LoggerUtils.res index 45f1c44b..7c110aff 100644 --- a/src/utility/logics/LoggerUtils.res +++ b/src/utility/logics/LoggerUtils.res @@ -5,6 +5,7 @@ let eventToStrMapper = (eventName: eventName) => { } let codePushVersionRef = ref(CP_NOT_STARTED) +let sdkVersionRef = ref(PACKAGE_JSON_NOT_STARTED) let logFileToObj = logFile => { [ ("timestamp", logFile.timestamp->JSON.Encode.string), @@ -34,6 +35,7 @@ let logFileToObj = logFile => { ), ("version", logFile.version->JSON.Encode.string), // repoversion of orca-android ("code_push_version", logFile.codePushVersion->JSON.Encode.string), + ("client_core_version", logFile.clientCoreVersion->JSON.Encode.string), ("value", logFile.value->JSON.Encode.string), ("internal_metadata", logFile.internalMetadata->JSON.Encode.string), ("session_id", logFile.sessionId->JSON.Encode.string), @@ -117,3 +119,33 @@ let getCodePushVersionNoFromRef = () => { | _ => "loading" } } + +type dataModule = {version: string} + +@val +external importStates: string => promise = "import" + +let getClientCoreVersion = () => { + if sdkVersionRef.contents == PACKAGE_JSON_NOT_STARTED { + sdkVersionRef := PACKAGE_JSON_LOADING + + importStates("./../../../package.json") + ->Promise.then(res => { + sdkVersionRef := PACKAGE_JSON_LOADED(res.version) + Promise.resolve() + }) + ->Promise.catch(_ => { + sdkVersionRef := PACKAGE_JSON_REFERENCE_ERROR + Promise.resolve() + }) + ->ignore + } +} + +let getClientCoreVersionNoFromRef = () => { + switch sdkVersionRef.contents { + | PACKAGE_JSON_LOADED(version) => version + | PACKAGE_JSON_REFERENCE_ERROR => "reference_error" + | _ => "loading" + } +} diff --git a/src/utility/logics/Window.res b/src/utility/logics/Window.res index 2d9bcd2a..795046fc 100644 --- a/src/utility/logics/Window.res +++ b/src/utility/logics/Window.res @@ -113,7 +113,15 @@ type messageHandlers = { type webKit = {messageHandlers?: messageHandlers} -@scope("window") external webKit: option = "webkit" +@scope("window") external webKit: Nullable.t = "webkit" + +type androidInterface = { + sdkInitialised: string => unit, + exitPaymentSheet: string => unit, + launchGPay: string => unit, +} + +@scope("window") external androidInterface: Nullable.t = "AndroidInterface" type billingContact = { addressLines: array,